##// END OF EJS Templates
fixed issues with gitsubmodule diffs
marcink -
r2233:07fce193 beta
parent child Browse files
Show More
@@ -1,517 +1,524 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
30 import markupsafe
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37
37 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.utils import EmptyChangeset
39
39
40
40
41 def wrap_to_table(str_):
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
46 </tr>
47 </table>''' % str_
47 </table>''' % str_
48
48
49
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
52 enable_comments=False):
53 """
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
55 proper message
56 """
56 """
57
57
58 if filenode_old is None:
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
60
61 if filenode_old.is_binary or filenode_new.is_binary:
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
63 stats = (0, 0)
64 size = 0
64 size = 0
65
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
75 stats = diff_processor.stat()
76 size = len(diff or '')
76 size = len(diff or '')
77 else:
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
79 'diff menu to display this diff'))
80 stats = (0, 0)
80 stats = (0, 0)
81 size = 0
81 size = 0
82
83 if not diff:
82 if not diff:
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
84 [filenode_new, filenode_old])
85 if submodules:
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
87 else:
84 diff = wrap_to_table(_('No changes detected'))
88 diff = wrap_to_table(_('No changes detected'))
85
89
86 cs1 = filenode_old.changeset.raw_id
90 cs1 = filenode_old.changeset.raw_id
87 cs2 = filenode_new.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
88
92
89 return size, cs1, cs2, diff, stats
93 return size, cs1, cs2, diff, stats
90
94
91
95
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
93 """
97 """
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
95
99
96 :param ignore_whitespace: ignore whitespaces in diff
100 :param ignore_whitespace: ignore whitespaces in diff
97 """
101 """
98 # make sure we pass in default context
102 # make sure we pass in default context
99 context = context or 3
103 context = context or 3
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
106 if submodules:
107 return ''
100
108
101 for filenode in (filenode_old, filenode_new):
109 for filenode in (filenode_old, filenode_new):
102 if not isinstance(filenode, FileNode):
110 if not isinstance(filenode, FileNode):
103 raise VCSError("Given object should be FileNode object, not %s"
111 raise VCSError("Given object should be FileNode object, not %s"
104 % filenode.__class__)
112 % filenode.__class__)
105
113
106 repo = filenode_new.changeset.repository
114 repo = filenode_new.changeset.repository
107 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
108 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
109
117
110 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
111 ignore_whitespace, context)
119 ignore_whitespace, context)
112
113 return vcs_gitdiff
120 return vcs_gitdiff
114
121
115
122
116 class DiffProcessor(object):
123 class DiffProcessor(object):
117 """
124 """
118 Give it a unified diff and it returns a list of the files that were
125 Give it a unified diff and it returns a list of the files that were
119 mentioned in the diff together with a dict of meta information that
126 mentioned in the diff together with a dict of meta information that
120 can be used to render it in a HTML template.
127 can be used to render it in a HTML template.
121 """
128 """
122 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
123
130
124 def __init__(self, diff, differ='diff', format='udiff'):
131 def __init__(self, diff, differ='diff', format='udiff'):
125 """
132 """
126 :param diff: a text in diff format or generator
133 :param diff: a text in diff format or generator
127 :param format: format of diff passed, `udiff` or `gitdiff`
134 :param format: format of diff passed, `udiff` or `gitdiff`
128 """
135 """
129 if isinstance(diff, basestring):
136 if isinstance(diff, basestring):
130 diff = [diff]
137 diff = [diff]
131
138
132 self.__udiff = diff
139 self.__udiff = diff
133 self.__format = format
140 self.__format = format
134 self.adds = 0
141 self.adds = 0
135 self.removes = 0
142 self.removes = 0
136
143
137 if isinstance(self.__udiff, basestring):
144 if isinstance(self.__udiff, basestring):
138 self.lines = iter(self.__udiff.splitlines(1))
145 self.lines = iter(self.__udiff.splitlines(1))
139
146
140 elif self.__format == 'gitdiff':
147 elif self.__format == 'gitdiff':
141 udiff_copy = self.copy_iterator()
148 udiff_copy = self.copy_iterator()
142 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
143 else:
150 else:
144 udiff_copy = self.copy_iterator()
151 udiff_copy = self.copy_iterator()
145 self.lines = imap(self.escaper, udiff_copy)
152 self.lines = imap(self.escaper, udiff_copy)
146
153
147 # Select a differ.
154 # Select a differ.
148 if differ == 'difflib':
155 if differ == 'difflib':
149 self.differ = self._highlight_line_difflib
156 self.differ = self._highlight_line_difflib
150 else:
157 else:
151 self.differ = self._highlight_line_udiff
158 self.differ = self._highlight_line_udiff
152
159
153 def escaper(self, string):
160 def escaper(self, string):
154 return markupsafe.escape(string)
161 return markupsafe.escape(string)
155
162
156 def copy_iterator(self):
163 def copy_iterator(self):
157 """
164 """
158 make a fresh copy of generator, we should not iterate thru
165 make a fresh copy of generator, we should not iterate thru
159 an original as it's needed for repeating operations on
166 an original as it's needed for repeating operations on
160 this instance of DiffProcessor
167 this instance of DiffProcessor
161 """
168 """
162 self.__udiff, iterator_copy = tee(self.__udiff)
169 self.__udiff, iterator_copy = tee(self.__udiff)
163 return iterator_copy
170 return iterator_copy
164
171
165 def _extract_rev(self, line1, line2):
172 def _extract_rev(self, line1, line2):
166 """
173 """
167 Extract the filename and revision hint from a line.
174 Extract the filename and revision hint from a line.
168 """
175 """
169
176
170 try:
177 try:
171 if line1.startswith('--- ') and line2.startswith('+++ '):
178 if line1.startswith('--- ') and line2.startswith('+++ '):
172 l1 = line1[4:].split(None, 1)
179 l1 = line1[4:].split(None, 1)
173 old_filename = (l1[0].replace('a/', '', 1)
180 old_filename = (l1[0].replace('a/', '', 1)
174 if len(l1) >= 1 else None)
181 if len(l1) >= 1 else None)
175 old_rev = l1[1] if len(l1) == 2 else 'old'
182 old_rev = l1[1] if len(l1) == 2 else 'old'
176
183
177 l2 = line2[4:].split(None, 1)
184 l2 = line2[4:].split(None, 1)
178 new_filename = (l2[0].replace('b/', '', 1)
185 new_filename = (l2[0].replace('b/', '', 1)
179 if len(l1) >= 1 else None)
186 if len(l1) >= 1 else None)
180 new_rev = l2[1] if len(l2) == 2 else 'new'
187 new_rev = l2[1] if len(l2) == 2 else 'new'
181
188
182 filename = (old_filename
189 filename = (old_filename
183 if old_filename != '/dev/null' else new_filename)
190 if old_filename != '/dev/null' else new_filename)
184
191
185 return filename, new_rev, old_rev
192 return filename, new_rev, old_rev
186 except (ValueError, IndexError):
193 except (ValueError, IndexError):
187 pass
194 pass
188
195
189 return None, None, None
196 return None, None, None
190
197
191 def _parse_gitdiff(self, diffiterator):
198 def _parse_gitdiff(self, diffiterator):
192 def line_decoder(l):
199 def line_decoder(l):
193 if l.startswith('+') and not l.startswith('+++'):
200 if l.startswith('+') and not l.startswith('+++'):
194 self.adds += 1
201 self.adds += 1
195 elif l.startswith('-') and not l.startswith('---'):
202 elif l.startswith('-') and not l.startswith('---'):
196 self.removes += 1
203 self.removes += 1
197 return l.decode('utf8', 'replace')
204 return l.decode('utf8', 'replace')
198
205
199 output = list(diffiterator)
206 output = list(diffiterator)
200 size = len(output)
207 size = len(output)
201
208
202 if size == 2:
209 if size == 2:
203 l = []
210 l = []
204 l.extend([output[0]])
211 l.extend([output[0]])
205 l.extend(output[1].splitlines(1))
212 l.extend(output[1].splitlines(1))
206 return map(line_decoder, l)
213 return map(line_decoder, l)
207 elif size == 1:
214 elif size == 1:
208 return map(line_decoder, output[0].splitlines(1))
215 return map(line_decoder, output[0].splitlines(1))
209 elif size == 0:
216 elif size == 0:
210 return []
217 return []
211
218
212 raise Exception('wrong size of diff %s' % size)
219 raise Exception('wrong size of diff %s' % size)
213
220
214 def _highlight_line_difflib(self, line, next_):
221 def _highlight_line_difflib(self, line, next_):
215 """
222 """
216 Highlight inline changes in both lines.
223 Highlight inline changes in both lines.
217 """
224 """
218
225
219 if line['action'] == 'del':
226 if line['action'] == 'del':
220 old, new = line, next_
227 old, new = line, next_
221 else:
228 else:
222 old, new = next_, line
229 old, new = next_, line
223
230
224 oldwords = re.split(r'(\W)', old['line'])
231 oldwords = re.split(r'(\W)', old['line'])
225 newwords = re.split(r'(\W)', new['line'])
232 newwords = re.split(r'(\W)', new['line'])
226
233
227 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
228
235
229 oldfragments, newfragments = [], []
236 oldfragments, newfragments = [], []
230 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
231 oldfrag = ''.join(oldwords[i1:i2])
238 oldfrag = ''.join(oldwords[i1:i2])
232 newfrag = ''.join(newwords[j1:j2])
239 newfrag = ''.join(newwords[j1:j2])
233 if tag != 'equal':
240 if tag != 'equal':
234 if oldfrag:
241 if oldfrag:
235 oldfrag = '<del>%s</del>' % oldfrag
242 oldfrag = '<del>%s</del>' % oldfrag
236 if newfrag:
243 if newfrag:
237 newfrag = '<ins>%s</ins>' % newfrag
244 newfrag = '<ins>%s</ins>' % newfrag
238 oldfragments.append(oldfrag)
245 oldfragments.append(oldfrag)
239 newfragments.append(newfrag)
246 newfragments.append(newfrag)
240
247
241 old['line'] = "".join(oldfragments)
248 old['line'] = "".join(oldfragments)
242 new['line'] = "".join(newfragments)
249 new['line'] = "".join(newfragments)
243
250
244 def _highlight_line_udiff(self, line, next_):
251 def _highlight_line_udiff(self, line, next_):
245 """
252 """
246 Highlight inline changes in both lines.
253 Highlight inline changes in both lines.
247 """
254 """
248 start = 0
255 start = 0
249 limit = min(len(line['line']), len(next_['line']))
256 limit = min(len(line['line']), len(next_['line']))
250 while start < limit and line['line'][start] == next_['line'][start]:
257 while start < limit and line['line'][start] == next_['line'][start]:
251 start += 1
258 start += 1
252 end = -1
259 end = -1
253 limit -= start
260 limit -= start
254 while -end <= limit and line['line'][end] == next_['line'][end]:
261 while -end <= limit and line['line'][end] == next_['line'][end]:
255 end -= 1
262 end -= 1
256 end += 1
263 end += 1
257 if start or end:
264 if start or end:
258 def do(l):
265 def do(l):
259 last = end + len(l['line'])
266 last = end + len(l['line'])
260 if l['action'] == 'add':
267 if l['action'] == 'add':
261 tag = 'ins'
268 tag = 'ins'
262 else:
269 else:
263 tag = 'del'
270 tag = 'del'
264 l['line'] = '%s<%s>%s</%s>%s' % (
271 l['line'] = '%s<%s>%s</%s>%s' % (
265 l['line'][:start],
272 l['line'][:start],
266 tag,
273 tag,
267 l['line'][start:last],
274 l['line'][start:last],
268 tag,
275 tag,
269 l['line'][last:]
276 l['line'][last:]
270 )
277 )
271 do(line)
278 do(line)
272 do(next_)
279 do(next_)
273
280
274 def _parse_udiff(self):
281 def _parse_udiff(self):
275 """
282 """
276 Parse the diff an return data for the template.
283 Parse the diff an return data for the template.
277 """
284 """
278 lineiter = self.lines
285 lineiter = self.lines
279 files = []
286 files = []
280 try:
287 try:
281 line = lineiter.next()
288 line = lineiter.next()
282 # skip first context
289 # skip first context
283 skipfirst = True
290 skipfirst = True
284 while 1:
291 while 1:
285 # continue until we found the old file
292 # continue until we found the old file
286 if not line.startswith('--- '):
293 if not line.startswith('--- '):
287 line = lineiter.next()
294 line = lineiter.next()
288 continue
295 continue
289
296
290 chunks = []
297 chunks = []
291 filename, old_rev, new_rev = \
298 filename, old_rev, new_rev = \
292 self._extract_rev(line, lineiter.next())
299 self._extract_rev(line, lineiter.next())
293 files.append({
300 files.append({
294 'filename': filename,
301 'filename': filename,
295 'old_revision': old_rev,
302 'old_revision': old_rev,
296 'new_revision': new_rev,
303 'new_revision': new_rev,
297 'chunks': chunks
304 'chunks': chunks
298 })
305 })
299
306
300 line = lineiter.next()
307 line = lineiter.next()
301 while line:
308 while line:
302 match = self._chunk_re.match(line)
309 match = self._chunk_re.match(line)
303 if not match:
310 if not match:
304 break
311 break
305
312
306 lines = []
313 lines = []
307 chunks.append(lines)
314 chunks.append(lines)
308
315
309 old_line, old_end, new_line, new_end = \
316 old_line, old_end, new_line, new_end = \
310 [int(x or 1) for x in match.groups()[:-1]]
317 [int(x or 1) for x in match.groups()[:-1]]
311 old_line -= 1
318 old_line -= 1
312 new_line -= 1
319 new_line -= 1
313 context = len(match.groups()) == 5
320 context = len(match.groups()) == 5
314 old_end += old_line
321 old_end += old_line
315 new_end += new_line
322 new_end += new_line
316
323
317 if context:
324 if context:
318 if not skipfirst:
325 if not skipfirst:
319 lines.append({
326 lines.append({
320 'old_lineno': '...',
327 'old_lineno': '...',
321 'new_lineno': '...',
328 'new_lineno': '...',
322 'action': 'context',
329 'action': 'context',
323 'line': line,
330 'line': line,
324 })
331 })
325 else:
332 else:
326 skipfirst = False
333 skipfirst = False
327
334
328 line = lineiter.next()
335 line = lineiter.next()
329 while old_line < old_end or new_line < new_end:
336 while old_line < old_end or new_line < new_end:
330 if line:
337 if line:
331 command, line = line[0], line[1:]
338 command, line = line[0], line[1:]
332 else:
339 else:
333 command = ' '
340 command = ' '
334 affects_old = affects_new = False
341 affects_old = affects_new = False
335
342
336 # ignore those if we don't expect them
343 # ignore those if we don't expect them
337 if command in '#@':
344 if command in '#@':
338 continue
345 continue
339 elif command == '+':
346 elif command == '+':
340 affects_new = True
347 affects_new = True
341 action = 'add'
348 action = 'add'
342 elif command == '-':
349 elif command == '-':
343 affects_old = True
350 affects_old = True
344 action = 'del'
351 action = 'del'
345 else:
352 else:
346 affects_old = affects_new = True
353 affects_old = affects_new = True
347 action = 'unmod'
354 action = 'unmod'
348
355
349 old_line += affects_old
356 old_line += affects_old
350 new_line += affects_new
357 new_line += affects_new
351 lines.append({
358 lines.append({
352 'old_lineno': affects_old and old_line or '',
359 'old_lineno': affects_old and old_line or '',
353 'new_lineno': affects_new and new_line or '',
360 'new_lineno': affects_new and new_line or '',
354 'action': action,
361 'action': action,
355 'line': line
362 'line': line
356 })
363 })
357 line = lineiter.next()
364 line = lineiter.next()
358
365
359 except StopIteration:
366 except StopIteration:
360 pass
367 pass
361
368
362 # highlight inline changes
369 # highlight inline changes
363 for _ in files:
370 for _ in files:
364 for chunk in chunks:
371 for chunk in chunks:
365 lineiter = iter(chunk)
372 lineiter = iter(chunk)
366 #first = True
373 #first = True
367 try:
374 try:
368 while 1:
375 while 1:
369 line = lineiter.next()
376 line = lineiter.next()
370 if line['action'] != 'unmod':
377 if line['action'] != 'unmod':
371 nextline = lineiter.next()
378 nextline = lineiter.next()
372 if nextline['action'] == 'unmod' or \
379 if nextline['action'] == 'unmod' or \
373 nextline['action'] == line['action']:
380 nextline['action'] == line['action']:
374 continue
381 continue
375 self.differ(line, nextline)
382 self.differ(line, nextline)
376 except StopIteration:
383 except StopIteration:
377 pass
384 pass
378
385
379 return files
386 return files
380
387
381 def prepare(self):
388 def prepare(self):
382 """
389 """
383 Prepare the passed udiff for HTML rendering. It'l return a list
390 Prepare the passed udiff for HTML rendering. It'l return a list
384 of dicts
391 of dicts
385 """
392 """
386 return self._parse_udiff()
393 return self._parse_udiff()
387
394
388 def _safe_id(self, idstring):
395 def _safe_id(self, idstring):
389 """Make a string safe for including in an id attribute.
396 """Make a string safe for including in an id attribute.
390
397
391 The HTML spec says that id attributes 'must begin with
398 The HTML spec says that id attributes 'must begin with
392 a letter ([A-Za-z]) and may be followed by any number
399 a letter ([A-Za-z]) and may be followed by any number
393 of letters, digits ([0-9]), hyphens ("-"), underscores
400 of letters, digits ([0-9]), hyphens ("-"), underscores
394 ("_"), colons (":"), and periods (".")'. These regexps
401 ("_"), colons (":"), and periods (".")'. These regexps
395 are slightly over-zealous, in that they remove colons
402 are slightly over-zealous, in that they remove colons
396 and periods unnecessarily.
403 and periods unnecessarily.
397
404
398 Whitespace is transformed into underscores, and then
405 Whitespace is transformed into underscores, and then
399 anything which is not a hyphen or a character that
406 anything which is not a hyphen or a character that
400 matches \w (alphanumerics and underscore) is removed.
407 matches \w (alphanumerics and underscore) is removed.
401
408
402 """
409 """
403 # Transform all whitespace to underscore
410 # Transform all whitespace to underscore
404 idstring = re.sub(r'\s', "_", '%s' % idstring)
411 idstring = re.sub(r'\s', "_", '%s' % idstring)
405 # Remove everything that is not a hyphen or a member of \w
412 # Remove everything that is not a hyphen or a member of \w
406 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
413 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
407 return idstring
414 return idstring
408
415
409 def raw_diff(self):
416 def raw_diff(self):
410 """
417 """
411 Returns raw string as udiff
418 Returns raw string as udiff
412 """
419 """
413 udiff_copy = self.copy_iterator()
420 udiff_copy = self.copy_iterator()
414 if self.__format == 'gitdiff':
421 if self.__format == 'gitdiff':
415 udiff_copy = self._parse_gitdiff(udiff_copy)
422 udiff_copy = self._parse_gitdiff(udiff_copy)
416 return u''.join(udiff_copy)
423 return u''.join(udiff_copy)
417
424
418 def as_html(self, table_class='code-difftable', line_class='line',
425 def as_html(self, table_class='code-difftable', line_class='line',
419 new_lineno_class='lineno old', old_lineno_class='lineno new',
426 new_lineno_class='lineno old', old_lineno_class='lineno new',
420 code_class='code', enable_comments=False):
427 code_class='code', enable_comments=False):
421 """
428 """
422 Return udiff as html table with customized css classes
429 Return udiff as html table with customized css classes
423 """
430 """
424 def _link_to_if(condition, label, url):
431 def _link_to_if(condition, label, url):
425 """
432 """
426 Generates a link if condition is meet or just the label if not.
433 Generates a link if condition is meet or just the label if not.
427 """
434 """
428
435
429 if condition:
436 if condition:
430 return '''<a href="%(url)s">%(label)s</a>''' % {
437 return '''<a href="%(url)s">%(label)s</a>''' % {
431 'url': url,
438 'url': url,
432 'label': label
439 'label': label
433 }
440 }
434 else:
441 else:
435 return label
442 return label
436 diff_lines = self.prepare()
443 diff_lines = self.prepare()
437 _html_empty = True
444 _html_empty = True
438 _html = []
445 _html = []
439 _html.append('''<table class="%(table_class)s">\n''' % {
446 _html.append('''<table class="%(table_class)s">\n''' % {
440 'table_class': table_class
447 'table_class': table_class
441 })
448 })
442 for diff in diff_lines:
449 for diff in diff_lines:
443 for line in diff['chunks']:
450 for line in diff['chunks']:
444 _html_empty = False
451 _html_empty = False
445 for change in line:
452 for change in line:
446 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
453 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
447 'lc': line_class,
454 'lc': line_class,
448 'action': change['action']
455 'action': change['action']
449 })
456 })
450 anchor_old_id = ''
457 anchor_old_id = ''
451 anchor_new_id = ''
458 anchor_new_id = ''
452 anchor_old = "%(filename)s_o%(oldline_no)s" % {
459 anchor_old = "%(filename)s_o%(oldline_no)s" % {
453 'filename': self._safe_id(diff['filename']),
460 'filename': self._safe_id(diff['filename']),
454 'oldline_no': change['old_lineno']
461 'oldline_no': change['old_lineno']
455 }
462 }
456 anchor_new = "%(filename)s_n%(oldline_no)s" % {
463 anchor_new = "%(filename)s_n%(oldline_no)s" % {
457 'filename': self._safe_id(diff['filename']),
464 'filename': self._safe_id(diff['filename']),
458 'oldline_no': change['new_lineno']
465 'oldline_no': change['new_lineno']
459 }
466 }
460 cond_old = (change['old_lineno'] != '...' and
467 cond_old = (change['old_lineno'] != '...' and
461 change['old_lineno'])
468 change['old_lineno'])
462 cond_new = (change['new_lineno'] != '...' and
469 cond_new = (change['new_lineno'] != '...' and
463 change['new_lineno'])
470 change['new_lineno'])
464 if cond_old:
471 if cond_old:
465 anchor_old_id = 'id="%s"' % anchor_old
472 anchor_old_id = 'id="%s"' % anchor_old
466 if cond_new:
473 if cond_new:
467 anchor_new_id = 'id="%s"' % anchor_new
474 anchor_new_id = 'id="%s"' % anchor_new
468 ###########################################################
475 ###########################################################
469 # OLD LINE NUMBER
476 # OLD LINE NUMBER
470 ###########################################################
477 ###########################################################
471 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
478 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
472 'a_id': anchor_old_id,
479 'a_id': anchor_old_id,
473 'olc': old_lineno_class
480 'olc': old_lineno_class
474 })
481 })
475
482
476 _html.append('''%(link)s''' % {
483 _html.append('''%(link)s''' % {
477 'link': _link_to_if(True, change['old_lineno'],
484 'link': _link_to_if(True, change['old_lineno'],
478 '#%s' % anchor_old)
485 '#%s' % anchor_old)
479 })
486 })
480 _html.append('''</td>\n''')
487 _html.append('''</td>\n''')
481 ###########################################################
488 ###########################################################
482 # NEW LINE NUMBER
489 # NEW LINE NUMBER
483 ###########################################################
490 ###########################################################
484
491
485 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
492 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
486 'a_id': anchor_new_id,
493 'a_id': anchor_new_id,
487 'nlc': new_lineno_class
494 'nlc': new_lineno_class
488 })
495 })
489
496
490 _html.append('''%(link)s''' % {
497 _html.append('''%(link)s''' % {
491 'link': _link_to_if(True, change['new_lineno'],
498 'link': _link_to_if(True, change['new_lineno'],
492 '#%s' % anchor_new)
499 '#%s' % anchor_new)
493 })
500 })
494 _html.append('''</td>\n''')
501 _html.append('''</td>\n''')
495 ###########################################################
502 ###########################################################
496 # CODE
503 # CODE
497 ###########################################################
504 ###########################################################
498 comments = '' if enable_comments else 'no-comment'
505 comments = '' if enable_comments else 'no-comment'
499 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
506 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
500 'cc': code_class,
507 'cc': code_class,
501 'inc': comments
508 'inc': comments
502 })
509 })
503 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
510 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
504 'code': change['line']
511 'code': change['line']
505 })
512 })
506 _html.append('''\t</td>''')
513 _html.append('''\t</td>''')
507 _html.append('''\n</tr>\n''')
514 _html.append('''\n</tr>\n''')
508 _html.append('''</table>''')
515 _html.append('''</table>''')
509 if _html_empty:
516 if _html_empty:
510 return None
517 return None
511 return ''.join(_html)
518 return ''.join(_html)
512
519
513 def stat(self):
520 def stat(self):
514 """
521 """
515 Returns tuple of added, and removed lines for this instance
522 Returns tuple of added, and removed lines for this instance
516 """
523 """
517 return self.adds, self.removes
524 return self.adds, self.removes
@@ -1,454 +1,460 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import RepositoryError
6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import VCSError
9 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 RemovedFileNode, SubModuleNode
14 RemovedFileNode, SubModuleNode
15 from rhodecode.lib.vcs.utils import safe_unicode
15 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18
18
19
19
20 class GitChangeset(BaseChangeset):
20 class GitChangeset(BaseChangeset):
21 """
21 """
22 Represents state of the repository at single revision.
22 Represents state of the repository at single revision.
23 """
23 """
24
24
25 def __init__(self, repository, revision):
25 def __init__(self, repository, revision):
26 self._stat_modes = {}
26 self._stat_modes = {}
27 self.repository = repository
27 self.repository = repository
28 self.raw_id = revision
28 self.raw_id = revision
29 self.revision = repository.revisions.index(revision)
29 self.revision = repository.revisions.index(revision)
30
30
31 self.short_id = self.raw_id[:12]
31 self.short_id = self.raw_id[:12]
32 self.id = self.raw_id
32 self.id = self.raw_id
33 try:
33 try:
34 commit = self.repository._repo.get_object(self.raw_id)
34 commit = self.repository._repo.get_object(self.raw_id)
35 except KeyError:
35 except KeyError:
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 self._commit = commit
37 self._commit = commit
38 self._tree_id = commit.tree
38 self._tree_id = commit.tree
39
39
40 try:
40 try:
41 self.message = safe_unicode(commit.message[:-1])
41 self.message = safe_unicode(commit.message[:-1])
42 # Always strip last eol
42 # Always strip last eol
43 except UnicodeDecodeError:
43 except UnicodeDecodeError:
44 self.message = commit.message[:-1].decode(commit.encoding
44 self.message = commit.message[:-1].decode(commit.encoding
45 or 'utf-8')
45 or 'utf-8')
46 #self.branch = None
46 #self.branch = None
47 self.tags = []
47 self.tags = []
48 #tree = self.repository.get_object(self._tree_id)
48 #tree = self.repository.get_object(self._tree_id)
49 self.nodes = {}
49 self.nodes = {}
50 self._paths = {}
50 self._paths = {}
51
51
52 @LazyProperty
52 @LazyProperty
53 def author(self):
53 def author(self):
54 return safe_unicode(self._commit.committer)
54 return safe_unicode(self._commit.committer)
55
55
56 @LazyProperty
56 @LazyProperty
57 def date(self):
57 def date(self):
58 return date_fromtimestamp(self._commit.commit_time,
58 return date_fromtimestamp(self._commit.commit_time,
59 self._commit.commit_timezone)
59 self._commit.commit_timezone)
60
60
61 @LazyProperty
61 @LazyProperty
62 def status(self):
62 def status(self):
63 """
63 """
64 Returns modified, added, removed, deleted files for current changeset
64 Returns modified, added, removed, deleted files for current changeset
65 """
65 """
66 return self.changed, self.added, self.removed
66 return self.changed, self.added, self.removed
67
67
68 @LazyProperty
68 @LazyProperty
69 def branch(self):
69 def branch(self):
70
70
71 heads = self.repository._heads(reverse=False)
71 heads = self.repository._heads(reverse=False)
72
72
73 ref = heads.get(self.raw_id)
73 ref = heads.get(self.raw_id)
74 if ref:
74 if ref:
75 return safe_unicode(ref)
75 return safe_unicode(ref)
76
76
77 def _fix_path(self, path):
77 def _fix_path(self, path):
78 """
78 """
79 Paths are stored without trailing slash so we need to get rid off it if
79 Paths are stored without trailing slash so we need to get rid off it if
80 needed.
80 needed.
81 """
81 """
82 if path.endswith('/'):
82 if path.endswith('/'):
83 path = path.rstrip('/')
83 path = path.rstrip('/')
84 return path
84 return path
85
85
86 def _get_id_for_path(self, path):
86 def _get_id_for_path(self, path):
87
87
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
89 if not path in self._paths:
89 if not path in self._paths:
90 path = path.strip('/')
90 path = path.strip('/')
91 # set root tree
91 # set root tree
92 tree = self.repository._repo[self._commit.tree]
92 tree = self.repository._repo[self._commit.tree]
93 if path == '':
93 if path == '':
94 self._paths[''] = tree.id
94 self._paths[''] = tree.id
95 return tree.id
95 return tree.id
96 splitted = path.split('/')
96 splitted = path.split('/')
97 dirs, name = splitted[:-1], splitted[-1]
97 dirs, name = splitted[:-1], splitted[-1]
98 curdir = ''
98 curdir = ''
99
99
100 # initially extract things from root dir
100 # initially extract things from root dir
101 for item, stat, id in tree.iteritems():
101 for item, stat, id in tree.iteritems():
102 if curdir:
102 if curdir:
103 name = '/'.join((curdir, item))
103 name = '/'.join((curdir, item))
104 else:
104 else:
105 name = item
105 name = item
106 self._paths[name] = id
106 self._paths[name] = id
107 self._stat_modes[name] = stat
107 self._stat_modes[name] = stat
108
108
109 for dir in dirs:
109 for dir in dirs:
110 if curdir:
110 if curdir:
111 curdir = '/'.join((curdir, dir))
111 curdir = '/'.join((curdir, dir))
112 else:
112 else:
113 curdir = dir
113 curdir = dir
114 dir_id = None
114 dir_id = None
115 for item, stat, id in tree.iteritems():
115 for item, stat, id in tree.iteritems():
116 if dir == item:
116 if dir == item:
117 dir_id = id
117 dir_id = id
118 if dir_id:
118 if dir_id:
119 # Update tree
119 # Update tree
120 tree = self.repository._repo[dir_id]
120 tree = self.repository._repo[dir_id]
121 if not isinstance(tree, objects.Tree):
121 if not isinstance(tree, objects.Tree):
122 raise ChangesetError('%s is not a directory' % curdir)
122 raise ChangesetError('%s is not a directory' % curdir)
123 else:
123 else:
124 raise ChangesetError('%s have not been found' % curdir)
124 raise ChangesetError('%s have not been found' % curdir)
125
125
126 # cache all items from the given traversed tree
126 # cache all items from the given traversed tree
127 for item, stat, id in tree.iteritems():
127 for item, stat, id in tree.iteritems():
128 if curdir:
128 if curdir:
129 name = '/'.join((curdir, item))
129 name = '/'.join((curdir, item))
130 else:
130 else:
131 name = item
131 name = item
132 self._paths[name] = id
132 self._paths[name] = id
133 self._stat_modes[name] = stat
133 self._stat_modes[name] = stat
134 if not path in self._paths:
134 if not path in self._paths:
135 raise NodeDoesNotExistError("There is no file nor directory "
135 raise NodeDoesNotExistError("There is no file nor directory "
136 "at the given path %r at revision %r"
136 "at the given path %r at revision %r"
137 % (path, self.short_id))
137 % (path, self.short_id))
138 return self._paths[path]
138 return self._paths[path]
139
139
140 def _get_kind(self, path):
140 def _get_kind(self, path):
141 id = self._get_id_for_path(path)
141 id = self._get_id_for_path(path)
142 obj = self.repository._repo[id]
142 obj = self.repository._repo[id]
143 if isinstance(obj, objects.Blob):
143 if isinstance(obj, objects.Blob):
144 return NodeKind.FILE
144 return NodeKind.FILE
145 elif isinstance(obj, objects.Tree):
145 elif isinstance(obj, objects.Tree):
146 return NodeKind.DIR
146 return NodeKind.DIR
147
147
148 def _get_file_nodes(self):
148 def _get_file_nodes(self):
149 return chain(*(t[2] for t in self.walk()))
149 return chain(*(t[2] for t in self.walk()))
150
150
151 @LazyProperty
151 @LazyProperty
152 def parents(self):
152 def parents(self):
153 """
153 """
154 Returns list of parents changesets.
154 Returns list of parents changesets.
155 """
155 """
156 return [self.repository.get_changeset(parent)
156 return [self.repository.get_changeset(parent)
157 for parent in self._commit.parents]
157 for parent in self._commit.parents]
158
158
159 def next(self, branch=None):
159 def next(self, branch=None):
160
160
161 if branch and self.branch != branch:
161 if branch and self.branch != branch:
162 raise VCSError('Branch option used on changeset not belonging '
162 raise VCSError('Branch option used on changeset not belonging '
163 'to that branch')
163 'to that branch')
164
164
165 def _next(changeset, branch):
165 def _next(changeset, branch):
166 try:
166 try:
167 next_ = changeset.revision + 1
167 next_ = changeset.revision + 1
168 next_rev = changeset.repository.revisions[next_]
168 next_rev = changeset.repository.revisions[next_]
169 except IndexError:
169 except IndexError:
170 raise ChangesetDoesNotExistError
170 raise ChangesetDoesNotExistError
171 cs = changeset.repository.get_changeset(next_rev)
171 cs = changeset.repository.get_changeset(next_rev)
172
172
173 if branch and branch != cs.branch:
173 if branch and branch != cs.branch:
174 return _next(cs, branch)
174 return _next(cs, branch)
175
175
176 return cs
176 return cs
177
177
178 return _next(self, branch)
178 return _next(self, branch)
179
179
180 def prev(self, branch=None):
180 def prev(self, branch=None):
181 if branch and self.branch != branch:
181 if branch and self.branch != branch:
182 raise VCSError('Branch option used on changeset not belonging '
182 raise VCSError('Branch option used on changeset not belonging '
183 'to that branch')
183 'to that branch')
184
184
185 def _prev(changeset, branch):
185 def _prev(changeset, branch):
186 try:
186 try:
187 prev_ = changeset.revision - 1
187 prev_ = changeset.revision - 1
188 if prev_ < 0:
188 if prev_ < 0:
189 raise IndexError
189 raise IndexError
190 prev_rev = changeset.repository.revisions[prev_]
190 prev_rev = changeset.repository.revisions[prev_]
191 except IndexError:
191 except IndexError:
192 raise ChangesetDoesNotExistError
192 raise ChangesetDoesNotExistError
193
193
194 cs = changeset.repository.get_changeset(prev_rev)
194 cs = changeset.repository.get_changeset(prev_rev)
195
195
196 if branch and branch != cs.branch:
196 if branch and branch != cs.branch:
197 return _prev(cs, branch)
197 return _prev(cs, branch)
198
198
199 return cs
199 return cs
200
200
201 return _prev(self, branch)
201 return _prev(self, branch)
202
202
203 def get_file_mode(self, path):
203 def get_file_mode(self, path):
204 """
204 """
205 Returns stat mode of the file at the given ``path``.
205 Returns stat mode of the file at the given ``path``.
206 """
206 """
207 # ensure path is traversed
207 # ensure path is traversed
208 self._get_id_for_path(path)
208 self._get_id_for_path(path)
209 return self._stat_modes[path]
209 return self._stat_modes[path]
210
210
211 def get_file_content(self, path):
211 def get_file_content(self, path):
212 """
212 """
213 Returns content of the file at given ``path``.
213 Returns content of the file at given ``path``.
214 """
214 """
215 id = self._get_id_for_path(path)
215 id = self._get_id_for_path(path)
216 blob = self.repository._repo[id]
216 blob = self.repository._repo[id]
217 return blob.as_pretty_string()
217 return blob.as_pretty_string()
218
218
219 def get_file_size(self, path):
219 def get_file_size(self, path):
220 """
220 """
221 Returns size of the file at given ``path``.
221 Returns size of the file at given ``path``.
222 """
222 """
223 id = self._get_id_for_path(path)
223 id = self._get_id_for_path(path)
224 blob = self.repository._repo[id]
224 blob = self.repository._repo[id]
225 return blob.raw_length()
225 return blob.raw_length()
226
226
227 def get_file_changeset(self, path):
227 def get_file_changeset(self, path):
228 """
228 """
229 Returns last commit of the file at the given ``path``.
229 Returns last commit of the file at the given ``path``.
230 """
230 """
231 node = self.get_node(path)
231 node = self.get_node(path)
232 return node.history[0]
232 return node.history[0]
233
233
234 def get_file_history(self, path):
234 def get_file_history(self, path):
235 """
235 """
236 Returns history of file as reversed list of ``Changeset`` objects for
236 Returns history of file as reversed list of ``Changeset`` objects for
237 which file at given ``path`` has been modified.
237 which file at given ``path`` has been modified.
238
238
239 TODO: This function now uses os underlying 'git' and 'grep' commands
239 TODO: This function now uses os underlying 'git' and 'grep' commands
240 which is generally not good. Should be replaced with algorithm
240 which is generally not good. Should be replaced with algorithm
241 iterating commits.
241 iterating commits.
242 """
242 """
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
244 self.id, path
244 self.id, path
245 )
245 )
246 so, se = self.repository.run_git_command(cmd)
246 so, se = self.repository.run_git_command(cmd)
247 ids = re.findall(r'\w{40}', so)
247 ids = re.findall(r'\w{40}', so)
248 return [self.repository.get_changeset(id) for id in ids]
248 return [self.repository.get_changeset(id) for id in ids]
249
249
250 def get_file_annotate(self, path):
250 def get_file_annotate(self, path):
251 """
251 """
252 Returns a list of three element tuples with lineno,changeset and line
252 Returns a list of three element tuples with lineno,changeset and line
253
253
254 TODO: This function now uses os underlying 'git' command which is
254 TODO: This function now uses os underlying 'git' command which is
255 generally not good. Should be replaced with algorithm iterating
255 generally not good. Should be replaced with algorithm iterating
256 commits.
256 commits.
257 """
257 """
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
259 # -l ==> outputs long shas (and we need all 40 characters)
259 # -l ==> outputs long shas (and we need all 40 characters)
260 # --root ==> doesn't put '^' character for bounderies
260 # --root ==> doesn't put '^' character for bounderies
261 # -r sha ==> blames for the given revision
261 # -r sha ==> blames for the given revision
262 so, se = self.repository.run_git_command(cmd)
262 so, se = self.repository.run_git_command(cmd)
263 annotate = []
263 annotate = []
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
265 ln_no = i + 1
265 ln_no = i + 1
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
268 return annotate
268 return annotate
269
269
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
271 subrepos=False):
271 subrepos=False):
272 """
272 """
273 Fills up given stream.
273 Fills up given stream.
274
274
275 :param stream: file like object.
275 :param stream: file like object.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
277 Default: ``tgz``.
277 Default: ``tgz``.
278 :param prefix: name of root directory in archive.
278 :param prefix: name of root directory in archive.
279 Default is repository name and changeset's raw_id joined with dash
279 Default is repository name and changeset's raw_id joined with dash
280 (``repo-tip.<KIND>``).
280 (``repo-tip.<KIND>``).
281 :param subrepos: include subrepos in this archive.
281 :param subrepos: include subrepos in this archive.
282
282
283 :raise ImproperArchiveTypeError: If given kind is wrong.
283 :raise ImproperArchiveTypeError: If given kind is wrong.
284 :raise VcsError: If given stream is None
284 :raise VcsError: If given stream is None
285
285
286 """
286 """
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
288 if kind not in allowed_kinds:
288 if kind not in allowed_kinds:
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
290 'of %s', allowed_kinds)
290 'of %s', allowed_kinds)
291
291
292 if prefix is None:
292 if prefix is None:
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
294 elif prefix.startswith('/'):
294 elif prefix.startswith('/'):
295 raise VCSError("Prefix cannot start with leading slash")
295 raise VCSError("Prefix cannot start with leading slash")
296 elif prefix.strip() == '':
296 elif prefix.strip() == '':
297 raise VCSError("Prefix cannot be empty")
297 raise VCSError("Prefix cannot be empty")
298
298
299 if kind == 'zip':
299 if kind == 'zip':
300 frmt = 'zip'
300 frmt = 'zip'
301 else:
301 else:
302 frmt = 'tar'
302 frmt = 'tar'
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
304 self.raw_id)
304 self.raw_id)
305 if kind == 'tgz':
305 if kind == 'tgz':
306 cmd += ' | gzip -9'
306 cmd += ' | gzip -9'
307 elif kind == 'tbz2':
307 elif kind == 'tbz2':
308 cmd += ' | bzip2 -9'
308 cmd += ' | bzip2 -9'
309
309
310 if stream is None:
310 if stream is None:
311 raise VCSError('You need to pass in a valid stream for filling'
311 raise VCSError('You need to pass in a valid stream for filling'
312 ' with archival data')
312 ' with archival data')
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
314 cwd=self.repository.path)
314 cwd=self.repository.path)
315
315
316 buffer_size = 1024 * 8
316 buffer_size = 1024 * 8
317 chunk = popen.stdout.read(buffer_size)
317 chunk = popen.stdout.read(buffer_size)
318 while chunk:
318 while chunk:
319 stream.write(chunk)
319 stream.write(chunk)
320 chunk = popen.stdout.read(buffer_size)
320 chunk = popen.stdout.read(buffer_size)
321 # Make sure all descriptors would be read
321 # Make sure all descriptors would be read
322 popen.communicate()
322 popen.communicate()
323
323
324 def get_nodes(self, path):
324 def get_nodes(self, path):
325 if self._get_kind(path) != NodeKind.DIR:
325 if self._get_kind(path) != NodeKind.DIR:
326 raise ChangesetError("Directory does not exist for revision %r at "
326 raise ChangesetError("Directory does not exist for revision %r at "
327 " %r" % (self.revision, path))
327 " %r" % (self.revision, path))
328 path = self._fix_path(path)
328 path = self._fix_path(path)
329 id = self._get_id_for_path(path)
329 id = self._get_id_for_path(path)
330 tree = self.repository._repo[id]
330 tree = self.repository._repo[id]
331 dirnodes = []
331 dirnodes = []
332 filenodes = []
332 filenodes = []
333 als = self.repository.alias
333 als = self.repository.alias
334 for name, stat, id in tree.iteritems():
334 for name, stat, id in tree.iteritems():
335 if objects.S_ISGITLINK(stat):
335 if objects.S_ISGITLINK(stat):
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
337 alias=als))
337 alias=als))
338 continue
338 continue
339
339
340 obj = self.repository._repo.get_object(id)
340 obj = self.repository._repo.get_object(id)
341 if path != '':
341 if path != '':
342 obj_path = '/'.join((path, name))
342 obj_path = '/'.join((path, name))
343 else:
343 else:
344 obj_path = name
344 obj_path = name
345 if obj_path not in self._stat_modes:
345 if obj_path not in self._stat_modes:
346 self._stat_modes[obj_path] = stat
346 self._stat_modes[obj_path] = stat
347 if isinstance(obj, objects.Tree):
347 if isinstance(obj, objects.Tree):
348 dirnodes.append(DirNode(obj_path, changeset=self))
348 dirnodes.append(DirNode(obj_path, changeset=self))
349 elif isinstance(obj, objects.Blob):
349 elif isinstance(obj, objects.Blob):
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
351 else:
351 else:
352 raise ChangesetError("Requested object should be Tree "
352 raise ChangesetError("Requested object should be Tree "
353 "or Blob, is %r" % type(obj))
353 "or Blob, is %r" % type(obj))
354 nodes = dirnodes + filenodes
354 nodes = dirnodes + filenodes
355 for node in nodes:
355 for node in nodes:
356 if not node.path in self.nodes:
356 if not node.path in self.nodes:
357 self.nodes[node.path] = node
357 self.nodes[node.path] = node
358 nodes.sort()
358 nodes.sort()
359 return nodes
359 return nodes
360
360
361 def get_node(self, path):
361 def get_node(self, path):
362 if isinstance(path, unicode):
362 if isinstance(path, unicode):
363 path = path.encode('utf-8')
363 path = path.encode('utf-8')
364 path = self._fix_path(path)
364 path = self._fix_path(path)
365 if not path in self.nodes:
365 if not path in self.nodes:
366 try:
366 try:
367 id = self._get_id_for_path(path)
367 id_ = self._get_id_for_path(path)
368 except ChangesetError:
368 except ChangesetError:
369 raise NodeDoesNotExistError("Cannot find one of parents' "
369 raise NodeDoesNotExistError("Cannot find one of parents' "
370 "directories for a given path: %s" % path)
370 "directories for a given path: %s" % path)
371 obj = self.repository._repo.get_object(id)
371
372 als = self.repository.alias
373 _GL = lambda m: m and objects.S_ISGITLINK(m)
374 if _GL(self._stat_modes.get(path)):
375 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
376 else:
377 obj = self.repository._repo.get_object(id_)
378
372 if isinstance(obj, objects.Tree):
379 if isinstance(obj, objects.Tree):
373 if path == '':
380 if path == '':
374 node = RootNode(changeset=self)
381 node = RootNode(changeset=self)
375 else:
382 else:
376 node = DirNode(path, changeset=self)
383 node = DirNode(path, changeset=self)
377 node._tree = obj
384 node._tree = obj
378 elif isinstance(obj, objects.Blob):
385 elif isinstance(obj, objects.Blob):
379 node = FileNode(path, changeset=self)
386 node = FileNode(path, changeset=self)
380 node._blob = obj
387 node._blob = obj
381 else:
388 else:
382 raise NodeDoesNotExistError("There is no file nor directory "
389 raise NodeDoesNotExistError("There is no file nor directory "
383 "at the given path %r at revision %r"
390 "at the given path %r at revision %r"
384 % (path, self.short_id))
391 % (path, self.short_id))
385 # cache node
392 # cache node
386 self.nodes[path] = node
393 self.nodes[path] = node
387 return self.nodes[path]
394 return self.nodes[path]
388
395
389 @LazyProperty
396 @LazyProperty
390 def affected_files(self):
397 def affected_files(self):
391 """
398 """
392 Get's a fast accessible file changes for given changeset
399 Get's a fast accessible file changes for given changeset
393 """
400 """
394
401
395 return self.added + self.changed
402 return self.added + self.changed
396
403
397 @LazyProperty
404 @LazyProperty
398 def _diff_name_status(self):
405 def _diff_name_status(self):
399 output = []
406 output = []
400 for parent in self.parents:
407 for parent in self.parents:
401 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
408 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
402 so, se = self.repository.run_git_command(cmd)
409 so, se = self.repository.run_git_command(cmd)
403 output.append(so.strip())
410 output.append(so.strip())
404 return '\n'.join(output)
411 return '\n'.join(output)
405
412
406 def _get_paths_for_status(self, status):
413 def _get_paths_for_status(self, status):
407 """
414 """
408 Returns sorted list of paths for given ``status``.
415 Returns sorted list of paths for given ``status``.
409
416
410 :param status: one of: *added*, *modified* or *deleted*
417 :param status: one of: *added*, *modified* or *deleted*
411 """
418 """
412 paths = set()
419 paths = set()
413 char = status[0].upper()
420 char = status[0].upper()
414 for line in self._diff_name_status.splitlines():
421 for line in self._diff_name_status.splitlines():
415 if not line:
422 if not line:
416 continue
423 continue
417
424
418 if line.startswith(char):
425 if line.startswith(char):
419 splitted = line.split(char, 1)
426 splitted = line.split(char, 1)
420 if not len(splitted) == 2:
427 if not len(splitted) == 2:
421 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
422 "particularly that line: %s" % (self._diff_name_status,
429 "particularly that line: %s" % (self._diff_name_status,
423 line))
430 line))
424 _path = splitted[1].strip()
431 _path = splitted[1].strip()
425 paths.add(_path)
432 paths.add(_path)
426
427 return sorted(paths)
433 return sorted(paths)
428
434
429 @LazyProperty
435 @LazyProperty
430 def added(self):
436 def added(self):
431 """
437 """
432 Returns list of added ``FileNode`` objects.
438 Returns list of added ``FileNode`` objects.
433 """
439 """
434 if not self.parents:
440 if not self.parents:
435 return list(self._get_file_nodes())
441 return list(self._get_file_nodes())
436 return [self.get_node(path) for path in self._get_paths_for_status('added')]
442 return [self.get_node(path) for path in self._get_paths_for_status('added')]
437
443
438 @LazyProperty
444 @LazyProperty
439 def changed(self):
445 def changed(self):
440 """
446 """
441 Returns list of modified ``FileNode`` objects.
447 Returns list of modified ``FileNode`` objects.
442 """
448 """
443 if not self.parents:
449 if not self.parents:
444 return []
450 return []
445 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
451 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
446
452
447 @LazyProperty
453 @LazyProperty
448 def removed(self):
454 def removed(self):
449 """
455 """
450 Returns list of removed ``FileNode`` objects.
456 Returns list of removed ``FileNode`` objects.
451 """
457 """
452 if not self.parents:
458 if not self.parents:
453 return []
459 return []
454 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
460 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,600 +1,611 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.nodes
3 vcs.nodes
4 ~~~~~~~~~
4 ~~~~~~~~~
5
5
6 Module holding everything related to vcs nodes.
6 Module holding everything related to vcs nodes.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11 import os
11 import os
12 import stat
12 import stat
13 import posixpath
13 import posixpath
14 import mimetypes
14 import mimetypes
15
15
16 from pygments import lexers
16 from pygments import lexers
17
17
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
20 from rhodecode.lib.vcs.exceptions import NodeError
20 from rhodecode.lib.vcs.exceptions import NodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 from rhodecode.lib.utils import EmptyChangeset
22
23
23
24
24 class NodeKind:
25 class NodeKind:
25 SUBMODULE = -1
26 SUBMODULE = -1
26 DIR = 1
27 DIR = 1
27 FILE = 2
28 FILE = 2
28
29
29
30
30 class NodeState:
31 class NodeState:
31 ADDED = u'added'
32 ADDED = u'added'
32 CHANGED = u'changed'
33 CHANGED = u'changed'
33 NOT_CHANGED = u'not changed'
34 NOT_CHANGED = u'not changed'
34 REMOVED = u'removed'
35 REMOVED = u'removed'
35
36
36
37
37 class NodeGeneratorBase(object):
38 class NodeGeneratorBase(object):
38 """
39 """
39 Base class for removed added and changed filenodes, it's a lazy generator
40 Base class for removed added and changed filenodes, it's a lazy generator
40 class that will create filenodes only on iteration or call
41 class that will create filenodes only on iteration or call
41
42
42 The len method doesn't need to create filenodes at all
43 The len method doesn't need to create filenodes at all
43 """
44 """
44
45
45 def __init__(self, current_paths, cs):
46 def __init__(self, current_paths, cs):
46 self.cs = cs
47 self.cs = cs
47 self.current_paths = current_paths
48 self.current_paths = current_paths
48
49
49 def __call__(self):
50 def __call__(self):
50 return [n for n in self]
51 return [n for n in self]
51
52
52 def __getslice__(self, i, j):
53 def __getslice__(self, i, j):
53 for p in self.current_paths[i:j]:
54 for p in self.current_paths[i:j]:
54 yield self.cs.get_node(p)
55 yield self.cs.get_node(p)
55
56
56 def __len__(self):
57 def __len__(self):
57 return len(self.current_paths)
58 return len(self.current_paths)
58
59
59 def __iter__(self):
60 def __iter__(self):
60 for p in self.current_paths:
61 for p in self.current_paths:
61 yield self.cs.get_node(p)
62 yield self.cs.get_node(p)
62
63
63
64
64 class AddedFileNodesGenerator(NodeGeneratorBase):
65 class AddedFileNodesGenerator(NodeGeneratorBase):
65 """
66 """
66 Class holding Added files for current changeset
67 Class holding Added files for current changeset
67 """
68 """
68 pass
69 pass
69
70
70
71
71 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 """
73 """
73 Class holding Changed files for current changeset
74 Class holding Changed files for current changeset
74 """
75 """
75 pass
76 pass
76
77
77
78
78 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 """
80 """
80 Class holding removed files for current changeset
81 Class holding removed files for current changeset
81 """
82 """
82 def __iter__(self):
83 def __iter__(self):
83 for p in self.current_paths:
84 for p in self.current_paths:
84 yield RemovedFileNode(path=p)
85 yield RemovedFileNode(path=p)
85
86
86 def __getslice__(self, i, j):
87 def __getslice__(self, i, j):
87 for p in self.current_paths[i:j]:
88 for p in self.current_paths[i:j]:
88 yield RemovedFileNode(path=p)
89 yield RemovedFileNode(path=p)
89
90
90
91
91 class Node(object):
92 class Node(object):
92 """
93 """
93 Simplest class representing file or directory on repository. SCM backends
94 Simplest class representing file or directory on repository. SCM backends
94 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 directly.
96 directly.
96
97
97 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 only. Moreover, every single node is identified by the ``path`` attribute,
99 only. Moreover, every single node is identified by the ``path`` attribute,
99 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 """
101 """
101
102
102 def __init__(self, path, kind):
103 def __init__(self, path, kind):
103 if path.startswith('/'):
104 if path.startswith('/'):
104 raise NodeError("Cannot initialize Node objects with slash at "
105 raise NodeError("Cannot initialize Node objects with slash at "
105 "the beginning as only relative paths are supported")
106 "the beginning as only relative paths are supported")
106 self.path = path.rstrip('/')
107 self.path = path.rstrip('/')
107 if path == '' and kind != NodeKind.DIR:
108 if path == '' and kind != NodeKind.DIR:
108 raise NodeError("Only DirNode and its subclasses may be "
109 raise NodeError("Only DirNode and its subclasses may be "
109 "initialized with empty path")
110 "initialized with empty path")
110 self.kind = kind
111 self.kind = kind
111 #self.dirs, self.files = [], []
112 #self.dirs, self.files = [], []
112 if self.is_root() and not self.is_dir():
113 if self.is_root() and not self.is_dir():
113 raise NodeError("Root node cannot be FILE kind")
114 raise NodeError("Root node cannot be FILE kind")
114
115
115 @LazyProperty
116 @LazyProperty
116 def parent(self):
117 def parent(self):
117 parent_path = self.get_parent_path()
118 parent_path = self.get_parent_path()
118 if parent_path:
119 if parent_path:
119 if self.changeset:
120 if self.changeset:
120 return self.changeset.get_node(parent_path)
121 return self.changeset.get_node(parent_path)
121 return DirNode(parent_path)
122 return DirNode(parent_path)
122 return None
123 return None
123
124
124 @LazyProperty
125 @LazyProperty
125 def unicode_path(self):
126 def unicode_path(self):
126 return safe_unicode(self.path)
127 return safe_unicode(self.path)
127
128
128 @LazyProperty
129 @LazyProperty
129 def name(self):
130 def name(self):
130 """
131 """
131 Returns name of the node so if its path
132 Returns name of the node so if its path
132 then only last part is returned.
133 then only last part is returned.
133 """
134 """
134 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135
136
136 def _get_kind(self):
137 def _get_kind(self):
137 return self._kind
138 return self._kind
138
139
139 def _set_kind(self, kind):
140 def _set_kind(self, kind):
140 if hasattr(self, '_kind'):
141 if hasattr(self, '_kind'):
141 raise NodeError("Cannot change node's kind")
142 raise NodeError("Cannot change node's kind")
142 else:
143 else:
143 self._kind = kind
144 self._kind = kind
144 # Post setter check (path's trailing slash)
145 # Post setter check (path's trailing slash)
145 if self.path.endswith('/'):
146 if self.path.endswith('/'):
146 raise NodeError("Node's path cannot end with slash")
147 raise NodeError("Node's path cannot end with slash")
147
148
148 kind = property(_get_kind, _set_kind)
149 kind = property(_get_kind, _set_kind)
149
150
150 def __cmp__(self, other):
151 def __cmp__(self, other):
151 """
152 """
152 Comparator using name of the node, needed for quick list sorting.
153 Comparator using name of the node, needed for quick list sorting.
153 """
154 """
154 kind_cmp = cmp(self.kind, other.kind)
155 kind_cmp = cmp(self.kind, other.kind)
155 if kind_cmp:
156 if kind_cmp:
156 return kind_cmp
157 return kind_cmp
157 return cmp(self.name, other.name)
158 return cmp(self.name, other.name)
158
159
159 def __eq__(self, other):
160 def __eq__(self, other):
160 for attr in ['name', 'path', 'kind']:
161 for attr in ['name', 'path', 'kind']:
161 if getattr(self, attr) != getattr(other, attr):
162 if getattr(self, attr) != getattr(other, attr):
162 return False
163 return False
163 if self.is_file():
164 if self.is_file():
164 if self.content != other.content:
165 if self.content != other.content:
165 return False
166 return False
166 else:
167 else:
167 # For DirNode's check without entering each dir
168 # For DirNode's check without entering each dir
168 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 if self_nodes_paths != other_nodes_paths:
171 if self_nodes_paths != other_nodes_paths:
171 return False
172 return False
172 return True
173 return True
173
174
174 def __nq__(self, other):
175 def __nq__(self, other):
175 return not self.__eq__(other)
176 return not self.__eq__(other)
176
177
177 def __repr__(self):
178 def __repr__(self):
178 return '<%s %r>' % (self.__class__.__name__, self.path)
179 return '<%s %r>' % (self.__class__.__name__, self.path)
179
180
180 def __str__(self):
181 def __str__(self):
181 return self.__repr__()
182 return self.__repr__()
182
183
183 def __unicode__(self):
184 def __unicode__(self):
184 return self.name
185 return self.name
185
186
186 def get_parent_path(self):
187 def get_parent_path(self):
187 """
188 """
188 Returns node's parent path or empty string if node is root.
189 Returns node's parent path or empty string if node is root.
189 """
190 """
190 if self.is_root():
191 if self.is_root():
191 return ''
192 return ''
192 return posixpath.dirname(self.path.rstrip('/')) + '/'
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
193
194
194 def is_file(self):
195 def is_file(self):
195 """
196 """
196 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 otherwise.
198 otherwise.
198 """
199 """
199 return self.kind == NodeKind.FILE
200 return self.kind == NodeKind.FILE
200
201
201 def is_dir(self):
202 def is_dir(self):
202 """
203 """
203 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 otherwise.
205 otherwise.
205 """
206 """
206 return self.kind == NodeKind.DIR
207 return self.kind == NodeKind.DIR
207
208
208 def is_root(self):
209 def is_root(self):
209 """
210 """
210 Returns ``True`` if node is a root node and ``False`` otherwise.
211 Returns ``True`` if node is a root node and ``False`` otherwise.
211 """
212 """
212 return self.kind == NodeKind.DIR and self.path == ''
213 return self.kind == NodeKind.DIR and self.path == ''
213
214
214 def is_submodule(self):
215 def is_submodule(self):
215 """
216 """
216 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 otherwise.
218 otherwise.
218 """
219 """
219 return self.kind == NodeKind.SUBMODULE
220 return self.kind == NodeKind.SUBMODULE
220
221
221 @LazyProperty
222 @LazyProperty
222 def added(self):
223 def added(self):
223 return self.state is NodeState.ADDED
224 return self.state is NodeState.ADDED
224
225
225 @LazyProperty
226 @LazyProperty
226 def changed(self):
227 def changed(self):
227 return self.state is NodeState.CHANGED
228 return self.state is NodeState.CHANGED
228
229
229 @LazyProperty
230 @LazyProperty
230 def not_changed(self):
231 def not_changed(self):
231 return self.state is NodeState.NOT_CHANGED
232 return self.state is NodeState.NOT_CHANGED
232
233
233 @LazyProperty
234 @LazyProperty
234 def removed(self):
235 def removed(self):
235 return self.state is NodeState.REMOVED
236 return self.state is NodeState.REMOVED
236
237
237
238
238 class FileNode(Node):
239 class FileNode(Node):
239 """
240 """
240 Class representing file nodes.
241 Class representing file nodes.
241
242
242 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: content: if given arbitrary sets content of the file
244 :attribute: content: if given arbitrary sets content of the file
244 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 """
247 """
247
248
248 def __init__(self, path, content=None, changeset=None, mode=None):
249 def __init__(self, path, content=None, changeset=None, mode=None):
249 """
250 """
250 Only one of ``content`` and ``changeset`` may be given. Passing both
251 Only one of ``content`` and ``changeset`` may be given. Passing both
251 would raise ``NodeError`` exception.
252 would raise ``NodeError`` exception.
252
253
253 :param path: relative path to the node
254 :param path: relative path to the node
254 :param content: content may be passed to constructor
255 :param content: content may be passed to constructor
255 :param changeset: if given, will use it to lazily fetch content
256 :param changeset: if given, will use it to lazily fetch content
256 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 """
258 """
258
259
259 if content and changeset:
260 if content and changeset:
260 raise NodeError("Cannot use both content and changeset")
261 raise NodeError("Cannot use both content and changeset")
261 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 self.changeset = changeset
263 self.changeset = changeset
263 self._content = content
264 self._content = content
264 self._mode = mode or 0100644
265 self._mode = mode or 0100644
265
266
266 @LazyProperty
267 @LazyProperty
267 def mode(self):
268 def mode(self):
268 """
269 """
269 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 use value given at initialization or 0100644 (default).
271 use value given at initialization or 0100644 (default).
271 """
272 """
272 if self.changeset:
273 if self.changeset:
273 mode = self.changeset.get_file_mode(self.path)
274 mode = self.changeset.get_file_mode(self.path)
274 else:
275 else:
275 mode = self._mode
276 mode = self._mode
276 return mode
277 return mode
277
278
278 @property
279 @property
279 def content(self):
280 def content(self):
280 """
281 """
281 Returns lazily content of the FileNode. If possible, would try to
282 Returns lazily content of the FileNode. If possible, would try to
282 decode content from UTF-8.
283 decode content from UTF-8.
283 """
284 """
284 if self.changeset:
285 if self.changeset:
285 content = self.changeset.get_file_content(self.path)
286 content = self.changeset.get_file_content(self.path)
286 else:
287 else:
287 content = self._content
288 content = self._content
288
289
289 if bool(content and '\0' in content):
290 if bool(content and '\0' in content):
290 return content
291 return content
291 return safe_unicode(content)
292 return safe_unicode(content)
292
293
293 @LazyProperty
294 @LazyProperty
294 def size(self):
295 def size(self):
295 if self.changeset:
296 if self.changeset:
296 return self.changeset.get_file_size(self.path)
297 return self.changeset.get_file_size(self.path)
297 raise NodeError("Cannot retrieve size of the file without related "
298 raise NodeError("Cannot retrieve size of the file without related "
298 "changeset attribute")
299 "changeset attribute")
299
300
300 @LazyProperty
301 @LazyProperty
301 def message(self):
302 def message(self):
302 if self.changeset:
303 if self.changeset:
303 return self.last_changeset.message
304 return self.last_changeset.message
304 raise NodeError("Cannot retrieve message of the file without related "
305 raise NodeError("Cannot retrieve message of the file without related "
305 "changeset attribute")
306 "changeset attribute")
306
307
307 @LazyProperty
308 @LazyProperty
308 def last_changeset(self):
309 def last_changeset(self):
309 if self.changeset:
310 if self.changeset:
310 return self.changeset.get_file_changeset(self.path)
311 return self.changeset.get_file_changeset(self.path)
311 raise NodeError("Cannot retrieve last changeset of the file without "
312 raise NodeError("Cannot retrieve last changeset of the file without "
312 "related changeset attribute")
313 "related changeset attribute")
313
314
314 def get_mimetype(self):
315 def get_mimetype(self):
315 """
316 """
316 Mimetype is calculated based on the file's content. If ``_mimetype``
317 Mimetype is calculated based on the file's content. If ``_mimetype``
317 attribute is available, it will be returned (backends which store
318 attribute is available, it will be returned (backends which store
318 mimetypes or can easily recognize them, should set this private
319 mimetypes or can easily recognize them, should set this private
319 attribute to indicate that type should *NOT* be calculated).
320 attribute to indicate that type should *NOT* be calculated).
320 """
321 """
321 if hasattr(self, '_mimetype'):
322 if hasattr(self, '_mimetype'):
322 if (isinstance(self._mimetype, (tuple, list,)) and
323 if (isinstance(self._mimetype, (tuple, list,)) and
323 len(self._mimetype) == 2):
324 len(self._mimetype) == 2):
324 return self._mimetype
325 return self._mimetype
325 else:
326 else:
326 raise NodeError('given _mimetype attribute must be an 2 '
327 raise NodeError('given _mimetype attribute must be an 2 '
327 'element list or tuple')
328 'element list or tuple')
328
329
329 mtype, encoding = mimetypes.guess_type(self.name)
330 mtype, encoding = mimetypes.guess_type(self.name)
330
331
331 if mtype is None:
332 if mtype is None:
332 if self.is_binary:
333 if self.is_binary:
333 mtype = 'application/octet-stream'
334 mtype = 'application/octet-stream'
334 encoding = None
335 encoding = None
335 else:
336 else:
336 mtype = 'text/plain'
337 mtype = 'text/plain'
337 encoding = None
338 encoding = None
338 return mtype, encoding
339 return mtype, encoding
339
340
340 @LazyProperty
341 @LazyProperty
341 def mimetype(self):
342 def mimetype(self):
342 """
343 """
343 Wrapper around full mimetype info. It returns only type of fetched
344 Wrapper around full mimetype info. It returns only type of fetched
344 mimetype without the encoding part. use get_mimetype function to fetch
345 mimetype without the encoding part. use get_mimetype function to fetch
345 full set of (type,encoding)
346 full set of (type,encoding)
346 """
347 """
347 return self.get_mimetype()[0]
348 return self.get_mimetype()[0]
348
349
349 @LazyProperty
350 @LazyProperty
350 def mimetype_main(self):
351 def mimetype_main(self):
351 return self.mimetype.split('/')[0]
352 return self.mimetype.split('/')[0]
352
353
353 @LazyProperty
354 @LazyProperty
354 def lexer(self):
355 def lexer(self):
355 """
356 """
356 Returns pygment's lexer class. Would try to guess lexer taking file's
357 Returns pygment's lexer class. Would try to guess lexer taking file's
357 content, name and mimetype.
358 content, name and mimetype.
358 """
359 """
359 try:
360 try:
360 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 except lexers.ClassNotFound:
362 except lexers.ClassNotFound:
362 lexer = lexers.TextLexer()
363 lexer = lexers.TextLexer()
363 # returns first alias
364 # returns first alias
364 return lexer
365 return lexer
365
366
366 @LazyProperty
367 @LazyProperty
367 def lexer_alias(self):
368 def lexer_alias(self):
368 """
369 """
369 Returns first alias of the lexer guessed for this file.
370 Returns first alias of the lexer guessed for this file.
370 """
371 """
371 return self.lexer.aliases[0]
372 return self.lexer.aliases[0]
372
373
373 @LazyProperty
374 @LazyProperty
374 def history(self):
375 def history(self):
375 """
376 """
376 Returns a list of changeset for this file in which the file was changed
377 Returns a list of changeset for this file in which the file was changed
377 """
378 """
378 if self.changeset is None:
379 if self.changeset is None:
379 raise NodeError('Unable to get changeset for this FileNode')
380 raise NodeError('Unable to get changeset for this FileNode')
380 return self.changeset.get_file_history(self.path)
381 return self.changeset.get_file_history(self.path)
381
382
382 @LazyProperty
383 @LazyProperty
383 def annotate(self):
384 def annotate(self):
384 """
385 """
385 Returns a list of three element tuples with lineno,changeset and line
386 Returns a list of three element tuples with lineno,changeset and line
386 """
387 """
387 if self.changeset is None:
388 if self.changeset is None:
388 raise NodeError('Unable to get changeset for this FileNode')
389 raise NodeError('Unable to get changeset for this FileNode')
389 return self.changeset.get_file_annotate(self.path)
390 return self.changeset.get_file_annotate(self.path)
390
391
391 @LazyProperty
392 @LazyProperty
392 def state(self):
393 def state(self):
393 if not self.changeset:
394 if not self.changeset:
394 raise NodeError("Cannot check state of the node if it's not "
395 raise NodeError("Cannot check state of the node if it's not "
395 "linked with changeset")
396 "linked with changeset")
396 elif self.path in (node.path for node in self.changeset.added):
397 elif self.path in (node.path for node in self.changeset.added):
397 return NodeState.ADDED
398 return NodeState.ADDED
398 elif self.path in (node.path for node in self.changeset.changed):
399 elif self.path in (node.path for node in self.changeset.changed):
399 return NodeState.CHANGED
400 return NodeState.CHANGED
400 else:
401 else:
401 return NodeState.NOT_CHANGED
402 return NodeState.NOT_CHANGED
402
403
403 @property
404 @property
404 def is_binary(self):
405 def is_binary(self):
405 """
406 """
406 Returns True if file has binary content.
407 Returns True if file has binary content.
407 """
408 """
408 _bin = '\0' in self.content
409 _bin = '\0' in self.content
409 return _bin
410 return _bin
410
411
411 @LazyProperty
412 @LazyProperty
412 def extension(self):
413 def extension(self):
413 """Returns filenode extension"""
414 """Returns filenode extension"""
414 return self.name.split('.')[-1]
415 return self.name.split('.')[-1]
415
416
416 def is_executable(self):
417 def is_executable(self):
417 """
418 """
418 Returns ``True`` if file has executable flag turned on.
419 Returns ``True`` if file has executable flag turned on.
419 """
420 """
420 return bool(self.mode & stat.S_IXUSR)
421 return bool(self.mode & stat.S_IXUSR)
421
422
422 def __repr__(self):
423 def __repr__(self):
423 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 self.changeset.short_id)
425 self.changeset.short_id)
425
426
426
427
427 class RemovedFileNode(FileNode):
428 class RemovedFileNode(FileNode):
428 """
429 """
429 Dummy FileNode class - trying to access any public attribute except path,
430 Dummy FileNode class - trying to access any public attribute except path,
430 name, kind or state (or methods/attributes checking those two) would raise
431 name, kind or state (or methods/attributes checking those two) would raise
431 RemovedFileNodeError.
432 RemovedFileNodeError.
432 """
433 """
433 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
434 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
434 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
435 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
435
436
436 def __init__(self, path):
437 def __init__(self, path):
437 """
438 """
438 :param path: relative path to the node
439 :param path: relative path to the node
439 """
440 """
440 super(RemovedFileNode, self).__init__(path=path)
441 super(RemovedFileNode, self).__init__(path=path)
441
442
442 def __getattribute__(self, attr):
443 def __getattribute__(self, attr):
443 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
444 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
444 return super(RemovedFileNode, self).__getattribute__(attr)
445 return super(RemovedFileNode, self).__getattribute__(attr)
445 raise RemovedFileNodeError("Cannot access attribute %s on "
446 raise RemovedFileNodeError("Cannot access attribute %s on "
446 "RemovedFileNode" % attr)
447 "RemovedFileNode" % attr)
447
448
448 @LazyProperty
449 @LazyProperty
449 def state(self):
450 def state(self):
450 return NodeState.REMOVED
451 return NodeState.REMOVED
451
452
452
453
453 class DirNode(Node):
454 class DirNode(Node):
454 """
455 """
455 DirNode stores list of files and directories within this node.
456 DirNode stores list of files and directories within this node.
456 Nodes may be used standalone but within repository context they
457 Nodes may be used standalone but within repository context they
457 lazily fetch data within same repositorty's changeset.
458 lazily fetch data within same repositorty's changeset.
458 """
459 """
459
460
460 def __init__(self, path, nodes=(), changeset=None):
461 def __init__(self, path, nodes=(), changeset=None):
461 """
462 """
462 Only one of ``nodes`` and ``changeset`` may be given. Passing both
463 Only one of ``nodes`` and ``changeset`` may be given. Passing both
463 would raise ``NodeError`` exception.
464 would raise ``NodeError`` exception.
464
465
465 :param path: relative path to the node
466 :param path: relative path to the node
466 :param nodes: content may be passed to constructor
467 :param nodes: content may be passed to constructor
467 :param changeset: if given, will use it to lazily fetch content
468 :param changeset: if given, will use it to lazily fetch content
468 :param size: always 0 for ``DirNode``
469 :param size: always 0 for ``DirNode``
469 """
470 """
470 if nodes and changeset:
471 if nodes and changeset:
471 raise NodeError("Cannot use both nodes and changeset")
472 raise NodeError("Cannot use both nodes and changeset")
472 super(DirNode, self).__init__(path, NodeKind.DIR)
473 super(DirNode, self).__init__(path, NodeKind.DIR)
473 self.changeset = changeset
474 self.changeset = changeset
474 self._nodes = nodes
475 self._nodes = nodes
475
476
476 @LazyProperty
477 @LazyProperty
477 def content(self):
478 def content(self):
478 raise NodeError("%s represents a dir and has no ``content`` attribute"
479 raise NodeError("%s represents a dir and has no ``content`` attribute"
479 % self)
480 % self)
480
481
481 @LazyProperty
482 @LazyProperty
482 def nodes(self):
483 def nodes(self):
483 if self.changeset:
484 if self.changeset:
484 nodes = self.changeset.get_nodes(self.path)
485 nodes = self.changeset.get_nodes(self.path)
485 else:
486 else:
486 nodes = self._nodes
487 nodes = self._nodes
487 self._nodes_dict = dict((node.path, node) for node in nodes)
488 self._nodes_dict = dict((node.path, node) for node in nodes)
488 return sorted(nodes)
489 return sorted(nodes)
489
490
490 @LazyProperty
491 @LazyProperty
491 def files(self):
492 def files(self):
492 return sorted((node for node in self.nodes if node.is_file()))
493 return sorted((node for node in self.nodes if node.is_file()))
493
494
494 @LazyProperty
495 @LazyProperty
495 def dirs(self):
496 def dirs(self):
496 return sorted((node for node in self.nodes if node.is_dir()))
497 return sorted((node for node in self.nodes if node.is_dir()))
497
498
498 def __iter__(self):
499 def __iter__(self):
499 for node in self.nodes:
500 for node in self.nodes:
500 yield node
501 yield node
501
502
502 def get_node(self, path):
503 def get_node(self, path):
503 """
504 """
504 Returns node from within this particular ``DirNode``, so it is now
505 Returns node from within this particular ``DirNode``, so it is now
505 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
506 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
506 'docs'. In order to access deeper nodes one must fetch nodes between
507 'docs'. In order to access deeper nodes one must fetch nodes between
507 them first - this would work::
508 them first - this would work::
508
509
509 docs = root.get_node('docs')
510 docs = root.get_node('docs')
510 docs.get_node('api').get_node('index.rst')
511 docs.get_node('api').get_node('index.rst')
511
512
512 :param: path - relative to the current node
513 :param: path - relative to the current node
513
514
514 .. note::
515 .. note::
515 To access lazily (as in example above) node have to be initialized
516 To access lazily (as in example above) node have to be initialized
516 with related changeset object - without it node is out of
517 with related changeset object - without it node is out of
517 context and may know nothing about anything else than nearest
518 context and may know nothing about anything else than nearest
518 (located at same level) nodes.
519 (located at same level) nodes.
519 """
520 """
520 try:
521 try:
521 path = path.rstrip('/')
522 path = path.rstrip('/')
522 if path == '':
523 if path == '':
523 raise NodeError("Cannot retrieve node without path")
524 raise NodeError("Cannot retrieve node without path")
524 self.nodes # access nodes first in order to set _nodes_dict
525 self.nodes # access nodes first in order to set _nodes_dict
525 paths = path.split('/')
526 paths = path.split('/')
526 if len(paths) == 1:
527 if len(paths) == 1:
527 if not self.is_root():
528 if not self.is_root():
528 path = '/'.join((self.path, paths[0]))
529 path = '/'.join((self.path, paths[0]))
529 else:
530 else:
530 path = paths[0]
531 path = paths[0]
531 return self._nodes_dict[path]
532 return self._nodes_dict[path]
532 elif len(paths) > 1:
533 elif len(paths) > 1:
533 if self.changeset is None:
534 if self.changeset is None:
534 raise NodeError("Cannot access deeper "
535 raise NodeError("Cannot access deeper "
535 "nodes without changeset")
536 "nodes without changeset")
536 else:
537 else:
537 path1, path2 = paths[0], '/'.join(paths[1:])
538 path1, path2 = paths[0], '/'.join(paths[1:])
538 return self.get_node(path1).get_node(path2)
539 return self.get_node(path1).get_node(path2)
539 else:
540 else:
540 raise KeyError
541 raise KeyError
541 except KeyError:
542 except KeyError:
542 raise NodeError("Node does not exist at %s" % path)
543 raise NodeError("Node does not exist at %s" % path)
543
544
544 @LazyProperty
545 @LazyProperty
545 def state(self):
546 def state(self):
546 raise NodeError("Cannot access state of DirNode")
547 raise NodeError("Cannot access state of DirNode")
547
548
548 @LazyProperty
549 @LazyProperty
549 def size(self):
550 def size(self):
550 size = 0
551 size = 0
551 for root, dirs, files in self.changeset.walk(self.path):
552 for root, dirs, files in self.changeset.walk(self.path):
552 for f in files:
553 for f in files:
553 size += f.size
554 size += f.size
554
555
555 return size
556 return size
556
557
557 def __repr__(self):
558 def __repr__(self):
558 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
559 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
559 self.changeset.short_id)
560 self.changeset.short_id)
560
561
561
562
562 class RootNode(DirNode):
563 class RootNode(DirNode):
563 """
564 """
564 DirNode being the root node of the repository.
565 DirNode being the root node of the repository.
565 """
566 """
566
567
567 def __init__(self, nodes=(), changeset=None):
568 def __init__(self, nodes=(), changeset=None):
568 super(RootNode, self).__init__(path='', nodes=nodes,
569 super(RootNode, self).__init__(path='', nodes=nodes,
569 changeset=changeset)
570 changeset=changeset)
570
571
571 def __repr__(self):
572 def __repr__(self):
572 return '<%s>' % self.__class__.__name__
573 return '<%s>' % self.__class__.__name__
573
574
574
575
575 class SubModuleNode(Node):
576 class SubModuleNode(Node):
576 """
577 """
577 represents a SubModule of Git or SubRepo of Mercurial
578 represents a SubModule of Git or SubRepo of Mercurial
578 """
579 """
580 is_binary = False
581 size = 0
582
579 def __init__(self, name, url=None, changeset=None, alias=None):
583 def __init__(self, name, url=None, changeset=None, alias=None):
580 self.path = name
584 self.path = name
581 self.kind = NodeKind.SUBMODULE
585 self.kind = NodeKind.SUBMODULE
582 self.alias = alias
586 self.alias = alias
583 # changeset MUST be STR !! since it can point to non-valid SCM
587 # we have to use emptyChangeset here since this can point to svn/git/hg
584 self.changeset = str(changeset)
588 # submodules we cannot get from repository
589 self.changeset = EmptyChangeset(str(changeset), alias=alias)
585 self.url = url or self._extract_submodule_url()
590 self.url = url or self._extract_submodule_url()
586
591
592 def __repr__(self):
593 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
594 self.changeset.short_id)
595
587 def _extract_submodule_url(self):
596 def _extract_submodule_url(self):
588 if self.alias == 'git':
597 if self.alias == 'git':
598 #TODO: find a way to parse gits submodule file and extract the
599 # linking URL
589 return self.path
600 return self.path
590 if self.alias == 'hg':
601 if self.alias == 'hg':
591 return self.path
602 return self.path
592
603
593 @LazyProperty
604 @LazyProperty
594 def name(self):
605 def name(self):
595 """
606 """
596 Returns name of the node so if its path
607 Returns name of the node so if its path
597 then only last part is returned.
608 then only last part is returned.
598 """
609 """
599 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
610 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
600 return u'%s @ %s' % (org, self.changeset[:12])
611 return u'%s @ %s' % (org, self.changeset.short_id)
General Comments 0
You need to be logged in to leave comments. Login now