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