##// END OF EJS Templates
vcs: added possibility to pre-load attributes for FileNodes.
marcink -
r1355:71f19ea6 default
parent child Browse files
Show More
@@ -1,527 +1,527 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT commit module
23 23 """
24 24
25 25 import re
26 26 import stat
27 27 from ConfigParser import ConfigParser
28 28 from itertools import chain
29 29 from StringIO import StringIO
30 30
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 from rhodecode.lib.datelib import utcdate_fromtimestamp
34 34 from rhodecode.lib.utils import safe_unicode, safe_str
35 35 from rhodecode.lib.utils2 import safe_int
36 36 from rhodecode.lib.vcs.conf import settings
37 37 from rhodecode.lib.vcs.backends import base
38 38 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
39 39 from rhodecode.lib.vcs.nodes import (
40 40 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
41 41 ChangedFileNodesGenerator, AddedFileNodesGenerator,
42 42 RemovedFileNodesGenerator)
43 43
44 44
45 45 class GitCommit(base.BaseCommit):
46 46 """
47 47 Represents state of the repository at single commit id.
48 48 """
49 49 _author_property = 'author'
50 50 _committer_property = 'committer'
51 51 _date_property = 'commit_time'
52 52 _date_tz_property = 'commit_timezone'
53 53 _message_property = 'message'
54 54 _parents_property = 'parents'
55 55
56 56 _filter_pre_load = [
57 57 # done through a more complex tree walk on parents
58 58 "affected_files",
59 59 # based on repository cached property
60 60 "branch",
61 61 # done through subprocess not remote call
62 62 "children",
63 63 # done through a more complex tree walk on parents
64 64 "status",
65 65 # mercurial specific property not supported here
66 66 "_file_paths",
67 67 ]
68 68
69 69 def __init__(self, repository, raw_id, idx, pre_load=None):
70 70 self.repository = repository
71 71 self._remote = repository._remote
72 72 # TODO: johbo: Tweak of raw_id should not be necessary
73 73 self.raw_id = safe_str(raw_id)
74 74 self.idx = idx
75 75
76 76 self._set_bulk_properties(pre_load)
77 77
78 78 # caches
79 79 self._stat_modes = {} # stat info for paths
80 80 self._paths = {} # path processed with parse_tree
81 81 self.nodes = {}
82 82 self._submodules = None
83 83
84 84 def _set_bulk_properties(self, pre_load):
85 85 if not pre_load:
86 86 return
87 87 pre_load = [entry for entry in pre_load
88 88 if entry not in self._filter_pre_load]
89 89 if not pre_load:
90 90 return
91 91
92 92 result = self._remote.bulk_request(self.raw_id, pre_load)
93 93 for attr, value in result.items():
94 94 if attr in ["author", "message"]:
95 95 if value:
96 96 value = safe_unicode(value)
97 97 elif attr == "date":
98 98 value = utcdate_fromtimestamp(*value)
99 99 elif attr == "parents":
100 100 value = self._make_commits(value)
101 101 self.__dict__[attr] = value
102 102
103 103 @LazyProperty
104 104 def _commit(self):
105 105 return self._remote[self.raw_id]
106 106
107 107 @LazyProperty
108 108 def _tree_id(self):
109 109 return self._remote[self._commit['tree']]['id']
110 110
111 111 @LazyProperty
112 112 def id(self):
113 113 return self.raw_id
114 114
115 115 @LazyProperty
116 116 def short_id(self):
117 117 return self.raw_id[:12]
118 118
119 119 @LazyProperty
120 120 def message(self):
121 121 return safe_unicode(
122 122 self._remote.commit_attribute(self.id, self._message_property))
123 123
124 124 @LazyProperty
125 125 def committer(self):
126 126 return safe_unicode(
127 127 self._remote.commit_attribute(self.id, self._committer_property))
128 128
129 129 @LazyProperty
130 130 def author(self):
131 131 return safe_unicode(
132 132 self._remote.commit_attribute(self.id, self._author_property))
133 133
134 134 @LazyProperty
135 135 def date(self):
136 136 unix_ts, tz = self._remote.get_object_attrs(
137 137 self.raw_id, self._date_property, self._date_tz_property)
138 138 return utcdate_fromtimestamp(unix_ts, tz)
139 139
140 140 @LazyProperty
141 141 def status(self):
142 142 """
143 143 Returns modified, added, removed, deleted files for current commit
144 144 """
145 145 return self.changed, self.added, self.removed
146 146
147 147 @LazyProperty
148 148 def tags(self):
149 149 tags = [safe_unicode(name) for name,
150 150 commit_id in self.repository.tags.iteritems()
151 151 if commit_id == self.raw_id]
152 152 return tags
153 153
154 154 @LazyProperty
155 155 def branch(self):
156 156 for name, commit_id in self.repository.branches.iteritems():
157 157 if commit_id == self.raw_id:
158 158 return safe_unicode(name)
159 159 return None
160 160
161 161 def _get_id_for_path(self, path):
162 162 path = safe_str(path)
163 163 if path in self._paths:
164 164 return self._paths[path]
165 165
166 166 tree_id = self._tree_id
167 167
168 168 path = path.strip('/')
169 169 if path == '':
170 170 data = [tree_id, "tree"]
171 171 self._paths[''] = data
172 172 return data
173 173
174 174 parts = path.split('/')
175 175 dirs, name = parts[:-1], parts[-1]
176 176 cur_dir = ''
177 177
178 178 # initially extract things from root dir
179 179 tree_items = self._remote.tree_items(tree_id)
180 180 self._process_tree_items(tree_items, cur_dir)
181 181
182 182 for dir in dirs:
183 183 if cur_dir:
184 184 cur_dir = '/'.join((cur_dir, dir))
185 185 else:
186 186 cur_dir = dir
187 187 dir_id = None
188 188 for item, stat_, id_, type_ in tree_items:
189 189 if item == dir:
190 190 dir_id = id_
191 191 break
192 192 if dir_id:
193 193 if type_ != "tree":
194 194 raise CommitError('%s is not a directory' % cur_dir)
195 195 # update tree
196 196 tree_items = self._remote.tree_items(dir_id)
197 197 else:
198 198 raise CommitError('%s have not been found' % cur_dir)
199 199
200 200 # cache all items from the given traversed tree
201 201 self._process_tree_items(tree_items, cur_dir)
202 202
203 203 if path not in self._paths:
204 204 raise self.no_node_at_path(path)
205 205
206 206 return self._paths[path]
207 207
208 208 def _process_tree_items(self, items, cur_dir):
209 209 for item, stat_, id_, type_ in items:
210 210 if cur_dir:
211 211 name = '/'.join((cur_dir, item))
212 212 else:
213 213 name = item
214 214 self._paths[name] = [id_, type_]
215 215 self._stat_modes[name] = stat_
216 216
217 217 def _get_kind(self, path):
218 218 path_id, type_ = self._get_id_for_path(path)
219 219 if type_ == 'blob':
220 220 return NodeKind.FILE
221 221 elif type_ == 'tree':
222 222 return NodeKind.DIR
223 223 elif type == 'link':
224 224 return NodeKind.SUBMODULE
225 225 return None
226 226
227 227 def _get_filectx(self, path):
228 228 path = self._fix_path(path)
229 229 if self._get_kind(path) != NodeKind.FILE:
230 230 raise CommitError(
231 231 "File does not exist for commit %s at '%s'" %
232 232 (self.raw_id, path))
233 233 return path
234 234
235 235 def _get_file_nodes(self):
236 236 return chain(*(t[2] for t in self.walk()))
237 237
238 238 @LazyProperty
239 239 def parents(self):
240 240 """
241 241 Returns list of parent commits.
242 242 """
243 243 parent_ids = self._remote.commit_attribute(
244 244 self.id, self._parents_property)
245 245 return self._make_commits(parent_ids)
246 246
247 247 @LazyProperty
248 248 def children(self):
249 249 """
250 250 Returns list of child commits.
251 251 """
252 252 rev_filter = settings.GIT_REV_FILTER
253 253 output, __ = self.repository.run_git_command(
254 254 ['rev-list', '--children'] + rev_filter)
255 255
256 256 child_ids = []
257 257 pat = re.compile(r'^%s' % self.raw_id)
258 258 for l in output.splitlines():
259 259 if pat.match(l):
260 260 found_ids = l.split(' ')[1:]
261 261 child_ids.extend(found_ids)
262 262 return self._make_commits(child_ids)
263 263
264 264 def _make_commits(self, commit_ids):
265 265 return [self.repository.get_commit(commit_id=commit_id)
266 266 for commit_id in commit_ids]
267 267
268 268 def get_file_mode(self, path):
269 269 """
270 270 Returns stat mode of the file at the given `path`.
271 271 """
272 272 path = safe_str(path)
273 273 # ensure path is traversed
274 274 self._get_id_for_path(path)
275 275 return self._stat_modes[path]
276 276
277 277 def is_link(self, path):
278 278 return stat.S_ISLNK(self.get_file_mode(path))
279 279
280 280 def get_file_content(self, path):
281 281 """
282 282 Returns content of the file at given `path`.
283 283 """
284 284 id_, _ = self._get_id_for_path(path)
285 285 return self._remote.blob_as_pretty_string(id_)
286 286
287 287 def get_file_size(self, path):
288 288 """
289 289 Returns size of the file at given `path`.
290 290 """
291 291 id_, _ = self._get_id_for_path(path)
292 292 return self._remote.blob_raw_length(id_)
293 293
294 294 def get_file_history(self, path, limit=None, pre_load=None):
295 295 """
296 296 Returns history of file as reversed list of `GitCommit` objects for
297 297 which file at given `path` has been modified.
298 298
299 299 TODO: This function now uses an underlying 'git' command which works
300 300 quickly but ideally we should replace with an algorithm.
301 301 """
302 302 self._get_filectx(path)
303 303 f_path = safe_str(path)
304 304
305 305 cmd = ['log']
306 306 if limit:
307 307 cmd.extend(['-n', str(safe_int(limit, 0))])
308 308 cmd.extend(['--pretty=format: %H', '-s', self.raw_id, '--', f_path])
309 309
310 310 output, __ = self.repository.run_git_command(cmd)
311 311 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
312 312
313 313 return [
314 314 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
315 315 for commit_id in commit_ids]
316 316
317 317 # TODO: unused for now potential replacement for subprocess
318 318 def get_file_history_2(self, path, limit=None, pre_load=None):
319 319 """
320 320 Returns history of file as reversed list of `Commit` objects for
321 321 which file at given `path` has been modified.
322 322 """
323 323 self._get_filectx(path)
324 324 f_path = safe_str(path)
325 325
326 326 commit_ids = self._remote.get_file_history(f_path, self.id, limit)
327 327
328 328 return [
329 329 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
330 330 for commit_id in commit_ids]
331 331
332 332 def get_file_annotate(self, path, pre_load=None):
333 333 """
334 334 Returns a generator of four element tuples with
335 335 lineno, commit_id, commit lazy loader and line
336 336
337 337 TODO: This function now uses os underlying 'git' command which is
338 338 generally not good. Should be replaced with algorithm iterating
339 339 commits.
340 340 """
341 341 cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
342 342 # -l ==> outputs long shas (and we need all 40 characters)
343 343 # --root ==> doesn't put '^' character for bounderies
344 344 # -r commit_id ==> blames for the given commit
345 345 output, __ = self.repository.run_git_command(cmd)
346 346
347 347 for i, blame_line in enumerate(output.split('\n')[:-1]):
348 348 line_no = i + 1
349 349 commit_id, line = re.split(r' ', blame_line, 1)
350 350 yield (
351 351 line_no, commit_id,
352 352 lambda: self.repository.get_commit(commit_id=commit_id,
353 353 pre_load=pre_load),
354 354 line)
355 355
356 356 def get_nodes(self, path):
357 357 if self._get_kind(path) != NodeKind.DIR:
358 358 raise CommitError(
359 359 "Directory does not exist for commit %s at "
360 360 " '%s'" % (self.raw_id, path))
361 361 path = self._fix_path(path)
362 362 id_, _ = self._get_id_for_path(path)
363 363 tree_id = self._remote[id_]['id']
364 364 dirnodes = []
365 365 filenodes = []
366 366 alias = self.repository.alias
367 367 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
368 368 if type_ == 'link':
369 369 url = self._get_submodule_url('/'.join((path, name)))
370 370 dirnodes.append(SubModuleNode(
371 371 name, url=url, commit=id_, alias=alias))
372 372 continue
373 373
374 374 if path != '':
375 375 obj_path = '/'.join((path, name))
376 376 else:
377 377 obj_path = name
378 378 if obj_path not in self._stat_modes:
379 379 self._stat_modes[obj_path] = stat_
380 380
381 381 if type_ == 'tree':
382 382 dirnodes.append(DirNode(obj_path, commit=self))
383 383 elif type_ == 'blob':
384 384 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
385 385 else:
386 386 raise CommitError(
387 387 "Requested object should be Tree or Blob, is %s", type_)
388 388
389 389 nodes = dirnodes + filenodes
390 390 for node in nodes:
391 391 if node.path not in self.nodes:
392 392 self.nodes[node.path] = node
393 393 nodes.sort()
394 394 return nodes
395 395
396 def get_node(self, path):
396 def get_node(self, path, pre_load=None):
397 397 if isinstance(path, unicode):
398 398 path = path.encode('utf-8')
399 399 path = self._fix_path(path)
400 400 if path not in self.nodes:
401 401 try:
402 402 id_, type_ = self._get_id_for_path(path)
403 403 except CommitError:
404 404 raise NodeDoesNotExistError(
405 405 "Cannot find one of parents' directories for a given "
406 406 "path: %s" % path)
407 407
408 408 if type_ == 'link':
409 409 url = self._get_submodule_url(path)
410 410 node = SubModuleNode(path, url=url, commit=id_,
411 411 alias=self.repository.alias)
412 412 elif type_ == 'tree':
413 413 if path == '':
414 414 node = RootNode(commit=self)
415 415 else:
416 416 node = DirNode(path, commit=self)
417 417 elif type_ == 'blob':
418 node = FileNode(path, commit=self)
418 node = FileNode(path, commit=self, pre_load=pre_load)
419 419 else:
420 420 raise self.no_node_at_path(path)
421 421
422 422 # cache node
423 423 self.nodes[path] = node
424 424 return self.nodes[path]
425 425
426 426 @LazyProperty
427 427 def affected_files(self):
428 428 """
429 429 Gets a fast accessible file changes for given commit
430 430 """
431 431 added, modified, deleted = self._changes_cache
432 432 return list(added.union(modified).union(deleted))
433 433
434 434 @LazyProperty
435 435 def _changes_cache(self):
436 436 added = set()
437 437 modified = set()
438 438 deleted = set()
439 439 _r = self._remote
440 440
441 441 parents = self.parents
442 442 if not self.parents:
443 443 parents = [base.EmptyCommit()]
444 444 for parent in parents:
445 445 if isinstance(parent, base.EmptyCommit):
446 446 oid = None
447 447 else:
448 448 oid = parent.raw_id
449 449 changes = _r.tree_changes(oid, self.raw_id)
450 450 for (oldpath, newpath), (_, _), (_, _) in changes:
451 451 if newpath and oldpath:
452 452 modified.add(newpath)
453 453 elif newpath and not oldpath:
454 454 added.add(newpath)
455 455 elif not newpath and oldpath:
456 456 deleted.add(oldpath)
457 457 return added, modified, deleted
458 458
459 459 def _get_paths_for_status(self, status):
460 460 """
461 461 Returns sorted list of paths for given ``status``.
462 462
463 463 :param status: one of: *added*, *modified* or *deleted*
464 464 """
465 465 added, modified, deleted = self._changes_cache
466 466 return sorted({
467 467 'added': list(added),
468 468 'modified': list(modified),
469 469 'deleted': list(deleted)}[status]
470 470 )
471 471
472 472 @LazyProperty
473 473 def added(self):
474 474 """
475 475 Returns list of added ``FileNode`` objects.
476 476 """
477 477 if not self.parents:
478 478 return list(self._get_file_nodes())
479 479 return AddedFileNodesGenerator(
480 480 [n for n in self._get_paths_for_status('added')], self)
481 481
482 482 @LazyProperty
483 483 def changed(self):
484 484 """
485 485 Returns list of modified ``FileNode`` objects.
486 486 """
487 487 if not self.parents:
488 488 return []
489 489 return ChangedFileNodesGenerator(
490 490 [n for n in self._get_paths_for_status('modified')], self)
491 491
492 492 @LazyProperty
493 493 def removed(self):
494 494 """
495 495 Returns list of removed ``FileNode`` objects.
496 496 """
497 497 if not self.parents:
498 498 return []
499 499 return RemovedFileNodesGenerator(
500 500 [n for n in self._get_paths_for_status('deleted')], self)
501 501
502 502 def _get_submodule_url(self, submodule_path):
503 503 git_modules_path = '.gitmodules'
504 504
505 505 if self._submodules is None:
506 506 self._submodules = {}
507 507
508 508 try:
509 509 submodules_node = self.get_node(git_modules_path)
510 510 except NodeDoesNotExistError:
511 511 return None
512 512
513 513 content = submodules_node.content
514 514
515 515 # ConfigParser fails if there are whitespaces
516 516 content = '\n'.join(l.strip() for l in content.split('\n'))
517 517
518 518 parser = ConfigParser()
519 519 parser.readfp(StringIO(content))
520 520
521 521 for section in parser.sections():
522 522 path = parser.get(section, 'path')
523 523 url = parser.get(section, 'url')
524 524 if path and url:
525 525 self._submodules[path.strip('/')] = url
526 526
527 527 return self._submodules.get(submodule_path.strip('/'))
@@ -1,362 +1,362 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 HG commit module
23 23 """
24 24
25 25 import os
26 26
27 27 from zope.cachedescriptors.property import Lazy as LazyProperty
28 28
29 29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 30 from rhodecode.lib.utils import safe_str, safe_unicode
31 31 from rhodecode.lib.vcs import path as vcspath
32 32 from rhodecode.lib.vcs.backends import base
33 33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 34 from rhodecode.lib.vcs.exceptions import CommitError
35 35 from rhodecode.lib.vcs.nodes import (
36 36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 38 LargeFileNode, LARGEFILE_PREFIX)
39 39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40 40
41 41
42 42 class MercurialCommit(base.BaseCommit):
43 43 """
44 44 Represents state of the repository at the single commit.
45 45 """
46 46
47 47 _filter_pre_load = [
48 48 # git specific property not supported here
49 49 "_commit",
50 50 ]
51 51
52 52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 53 raw_id = safe_str(raw_id)
54 54
55 55 self.repository = repository
56 56 self._remote = repository._remote
57 57
58 58 self.raw_id = raw_id
59 59 self.idx = repository._sanitize_commit_idx(idx)
60 60
61 61 self._set_bulk_properties(pre_load)
62 62
63 63 # caches
64 64 self.nodes = {}
65 65
66 66 def _set_bulk_properties(self, pre_load):
67 67 if not pre_load:
68 68 return
69 69 pre_load = [entry for entry in pre_load
70 70 if entry not in self._filter_pre_load]
71 71 if not pre_load:
72 72 return
73 73
74 74 result = self._remote.bulk_request(self.idx, pre_load)
75 75 for attr, value in result.items():
76 76 if attr in ["author", "branch", "message"]:
77 77 value = safe_unicode(value)
78 78 elif attr == "affected_files":
79 79 value = map(safe_unicode, value)
80 80 elif attr == "date":
81 81 value = utcdate_fromtimestamp(*value)
82 82 elif attr in ["children", "parents"]:
83 83 value = self._make_commits(value)
84 84 self.__dict__[attr] = value
85 85
86 86 @LazyProperty
87 87 def tags(self):
88 88 tags = [name for name, commit_id in self.repository.tags.iteritems()
89 89 if commit_id == self.raw_id]
90 90 return tags
91 91
92 92 @LazyProperty
93 93 def branch(self):
94 94 return safe_unicode(self._remote.ctx_branch(self.idx))
95 95
96 96 @LazyProperty
97 97 def bookmarks(self):
98 98 bookmarks = [
99 99 name for name, commit_id in self.repository.bookmarks.iteritems()
100 100 if commit_id == self.raw_id]
101 101 return bookmarks
102 102
103 103 @LazyProperty
104 104 def message(self):
105 105 return safe_unicode(self._remote.ctx_description(self.idx))
106 106
107 107 @LazyProperty
108 108 def committer(self):
109 109 return safe_unicode(self.author)
110 110
111 111 @LazyProperty
112 112 def author(self):
113 113 return safe_unicode(self._remote.ctx_user(self.idx))
114 114
115 115 @LazyProperty
116 116 def date(self):
117 117 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
118 118
119 119 @LazyProperty
120 120 def status(self):
121 121 """
122 122 Returns modified, added, removed, deleted files for current commit
123 123 """
124 124 return self._remote.ctx_status(self.idx)
125 125
126 126 @LazyProperty
127 127 def _file_paths(self):
128 128 return self._remote.ctx_list(self.idx)
129 129
130 130 @LazyProperty
131 131 def _dir_paths(self):
132 132 p = list(set(get_dirs_for_path(*self._file_paths)))
133 133 p.insert(0, '')
134 134 return p
135 135
136 136 @LazyProperty
137 137 def _paths(self):
138 138 return self._dir_paths + self._file_paths
139 139
140 140 @LazyProperty
141 141 def id(self):
142 142 if self.last:
143 143 return u'tip'
144 144 return self.short_id
145 145
146 146 @LazyProperty
147 147 def short_id(self):
148 148 return self.raw_id[:12]
149 149
150 150 def _make_commits(self, indexes):
151 151 return [self.repository.get_commit(commit_idx=idx)
152 152 for idx in indexes if idx >= 0]
153 153
154 154 @LazyProperty
155 155 def parents(self):
156 156 """
157 157 Returns list of parent commits.
158 158 """
159 159 parents = self._remote.ctx_parents(self.idx)
160 160 return self._make_commits(parents)
161 161
162 162 @LazyProperty
163 163 def children(self):
164 164 """
165 165 Returns list of child commits.
166 166 """
167 167 children = self._remote.ctx_children(self.idx)
168 168 return self._make_commits(children)
169 169
170 170 def diff(self, ignore_whitespace=True, context=3):
171 171 result = self._remote.ctx_diff(
172 172 self.idx,
173 173 git=True, ignore_whitespace=ignore_whitespace, context=context)
174 174 diff = ''.join(result)
175 175 return MercurialDiff(diff)
176 176
177 177 def _fix_path(self, path):
178 178 """
179 179 Mercurial keeps filenodes as str so we need to encode from unicode
180 180 to str.
181 181 """
182 182 return safe_str(super(MercurialCommit, self)._fix_path(path))
183 183
184 184 def _get_kind(self, path):
185 185 path = self._fix_path(path)
186 186 if path in self._file_paths:
187 187 return NodeKind.FILE
188 188 elif path in self._dir_paths:
189 189 return NodeKind.DIR
190 190 else:
191 191 raise CommitError(
192 192 "Node does not exist at the given path '%s'" % (path, ))
193 193
194 194 def _get_filectx(self, path):
195 195 path = self._fix_path(path)
196 196 if self._get_kind(path) != NodeKind.FILE:
197 197 raise CommitError(
198 198 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
199 199 return path
200 200
201 201 def get_file_mode(self, path):
202 202 """
203 203 Returns stat mode of the file at the given ``path``.
204 204 """
205 205 path = self._get_filectx(path)
206 206 if 'x' in self._remote.fctx_flags(self.idx, path):
207 207 return base.FILEMODE_EXECUTABLE
208 208 else:
209 209 return base.FILEMODE_DEFAULT
210 210
211 211 def is_link(self, path):
212 212 path = self._get_filectx(path)
213 213 return 'l' in self._remote.fctx_flags(self.idx, path)
214 214
215 215 def get_file_content(self, path):
216 216 """
217 217 Returns content of the file at given ``path``.
218 218 """
219 219 path = self._get_filectx(path)
220 220 return self._remote.fctx_data(self.idx, path)
221 221
222 222 def get_file_size(self, path):
223 223 """
224 224 Returns size of the file at given ``path``.
225 225 """
226 226 path = self._get_filectx(path)
227 227 return self._remote.fctx_size(self.idx, path)
228 228
229 229 def get_file_history(self, path, limit=None, pre_load=None):
230 230 """
231 231 Returns history of file as reversed list of `MercurialCommit` objects
232 232 for which file at given ``path`` has been modified.
233 233 """
234 234 path = self._get_filectx(path)
235 235 hist = self._remote.file_history(self.idx, path, limit)
236 236 return [
237 237 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
238 238 for commit_id in hist]
239 239
240 240 def get_file_annotate(self, path, pre_load=None):
241 241 """
242 242 Returns a generator of four element tuples with
243 243 lineno, commit_id, commit lazy loader and line
244 244 """
245 245 result = self._remote.fctx_annotate(self.idx, path)
246 246
247 247 for ln_no, commit_id, content in result:
248 248 yield (
249 249 ln_no, commit_id,
250 250 lambda: self.repository.get_commit(commit_id=commit_id,
251 251 pre_load=pre_load),
252 252 content)
253 253
254 254 def get_nodes(self, path):
255 255 """
256 256 Returns combined ``DirNode`` and ``FileNode`` objects list representing
257 257 state of commit at the given ``path``. If node at the given ``path``
258 258 is not instance of ``DirNode``, CommitError would be raised.
259 259 """
260 260
261 261 if self._get_kind(path) != NodeKind.DIR:
262 262 raise CommitError(
263 263 "Directory does not exist for idx %s at '%s'" %
264 264 (self.idx, path))
265 265 path = self._fix_path(path)
266 266
267 267 filenodes = [
268 268 FileNode(f, commit=self) for f in self._file_paths
269 269 if os.path.dirname(f) == path]
270 270 # TODO: johbo: Check if this can be done in a more obvious way
271 271 dirs = path == '' and '' or [
272 272 d for d in self._dir_paths
273 273 if d and vcspath.dirname(d) == path]
274 274 dirnodes = [
275 275 DirNode(d, commit=self) for d in dirs
276 276 if os.path.dirname(d) == path]
277 277
278 278 alias = self.repository.alias
279 279 for k, vals in self._submodules.iteritems():
280 280 loc = vals[0]
281 281 commit = vals[1]
282 282 dirnodes.append(
283 283 SubModuleNode(k, url=loc, commit=commit, alias=alias))
284 284 nodes = dirnodes + filenodes
285 285 # cache nodes
286 286 for node in nodes:
287 287 self.nodes[node.path] = node
288 288 nodes.sort()
289 289
290 290 return nodes
291 291
292 def get_node(self, path):
292 def get_node(self, path, pre_load=None):
293 293 """
294 294 Returns `Node` object from the given `path`. If there is no node at
295 295 the given `path`, `NodeDoesNotExistError` would be raised.
296 296 """
297 297 path = self._fix_path(path)
298 298
299 299 if path not in self.nodes:
300 300 if path in self._file_paths:
301 node = FileNode(path, commit=self)
301 node = FileNode(path, commit=self, pre_load=pre_load)
302 302 elif path in self._dir_paths:
303 303 if path == '':
304 304 node = RootNode(commit=self)
305 305 else:
306 306 node = DirNode(path, commit=self)
307 307 else:
308 308 raise self.no_node_at_path(path)
309 309
310 310 # cache node
311 311 self.nodes[path] = node
312 312 return self.nodes[path]
313 313
314 314 def get_largefile_node(self, path):
315 315 path = os.path.join(LARGEFILE_PREFIX, path)
316 316
317 317 if self._remote.is_large_file(path):
318 318 # content of that file regular FileNode is the hash of largefile
319 319 file_id = self.get_file_content(path).strip()
320 320 if self._remote.in_store(file_id):
321 321 path = self._remote.store_path(file_id)
322 322 return LargeFileNode(path, commit=self)
323 323 elif self._remote.in_user_cache(file_id):
324 324 path = self._remote.store_path(file_id)
325 325 self._remote.link(file_id, path)
326 326 return LargeFileNode(path, commit=self)
327 327
328 328 @LazyProperty
329 329 def _submodules(self):
330 330 """
331 331 Returns a dictionary with submodule information from substate file
332 332 of hg repository.
333 333 """
334 334 return self._remote.ctx_substate(self.idx)
335 335
336 336 @LazyProperty
337 337 def affected_files(self):
338 338 """
339 339 Gets a fast accessible file changes for given commit
340 340 """
341 341 return self._remote.ctx_files(self.idx)
342 342
343 343 @property
344 344 def added(self):
345 345 """
346 346 Returns list of added ``FileNode`` objects.
347 347 """
348 348 return AddedFileNodesGenerator([n for n in self.status[1]], self)
349 349
350 350 @property
351 351 def changed(self):
352 352 """
353 353 Returns list of modified ``FileNode`` objects.
354 354 """
355 355 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
356 356
357 357 @property
358 358 def removed(self):
359 359 """
360 360 Returns list of removed ``FileNode`` objects.
361 361 """
362 362 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,236 +1,236 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SVN commit module
23 23 """
24 24
25 25
26 26 import dateutil.parser
27 27 from zope.cachedescriptors.property import Lazy as LazyProperty
28 28
29 29 from rhodecode.lib.utils import safe_str, safe_unicode
30 30 from rhodecode.lib.vcs import nodes, path as vcspath
31 31 from rhodecode.lib.vcs.backends import base
32 32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33 33
34 34
35 35 _SVN_PROP_TRUE = '*'
36 36
37 37
38 38 class SubversionCommit(base.BaseCommit):
39 39 """
40 40 Subversion specific implementation of commits
41 41
42 42 .. attribute:: branch
43 43
44 44 The Subversion backend does not support to assign branches to
45 45 specific commits. This attribute has always the value `None`.
46 46
47 47 """
48 48
49 49 def __init__(self, repository, commit_id):
50 50 self.repository = repository
51 51 self.idx = self.repository._get_commit_idx(commit_id)
52 52 self._svn_rev = self.idx + 1
53 53 self._remote = repository._remote
54 54 # TODO: handling of raw_id should be a method on repository itself,
55 55 # which knows how to translate commit index and commit id
56 56 self.raw_id = commit_id
57 57 self.short_id = commit_id
58 58 self.id = 'r%s' % (commit_id, )
59 59
60 60 # TODO: Implement the following placeholder attributes
61 61 self.nodes = {}
62 62 self.tags = []
63 63
64 64 @property
65 65 def author(self):
66 66 return safe_unicode(self._properties.get('svn:author'))
67 67
68 68 @property
69 69 def date(self):
70 70 return _date_from_svn_properties(self._properties)
71 71
72 72 @property
73 73 def message(self):
74 74 return safe_unicode(self._properties.get('svn:log'))
75 75
76 76 @LazyProperty
77 77 def _properties(self):
78 78 return self._remote.revision_properties(self._svn_rev)
79 79
80 80 @LazyProperty
81 81 def parents(self):
82 82 parent_idx = self.idx - 1
83 83 if parent_idx >= 0:
84 84 parent = self.repository.get_commit(commit_idx=parent_idx)
85 85 return [parent]
86 86 return []
87 87
88 88 @LazyProperty
89 89 def children(self):
90 90 child_idx = self.idx + 1
91 91 if child_idx < len(self.repository.commit_ids):
92 92 child = self.repository.get_commit(commit_idx=child_idx)
93 93 return [child]
94 94 return []
95 95
96 96 def get_file_mode(self, path):
97 97 # Note: Subversion flags files which are executable with a special
98 98 # property `svn:executable` which is set to the value ``"*"``.
99 99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
100 100 return base.FILEMODE_EXECUTABLE
101 101 else:
102 102 return base.FILEMODE_DEFAULT
103 103
104 104 def is_link(self, path):
105 105 # Note: Subversion has a flag for special files, the content of the
106 106 # file contains the type of that file.
107 107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
108 108 return self.get_file_content(path).startswith('link')
109 109 return False
110 110
111 111 def _get_file_property(self, path, name):
112 112 file_properties = self._remote.node_properties(
113 113 safe_str(path), self._svn_rev)
114 114 return file_properties.get(name)
115 115
116 116 def get_file_content(self, path):
117 117 path = self._fix_path(path)
118 118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
119 119
120 120 def get_file_size(self, path):
121 121 path = self._fix_path(path)
122 122 return self._remote.get_file_size(safe_str(path), self._svn_rev)
123 123
124 124 def get_file_history(self, path, limit=None, pre_load=None):
125 125 path = safe_str(self._fix_path(path))
126 126 history = self._remote.node_history(path, self._svn_rev, limit)
127 127 return [
128 128 self.repository.get_commit(commit_id=str(svn_rev))
129 129 for svn_rev in history]
130 130
131 131 def get_file_annotate(self, path, pre_load=None):
132 132 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
133 133
134 134 for zero_based_line_no, svn_rev, content in result:
135 135 commit_id = str(svn_rev)
136 136 line_no = zero_based_line_no + 1
137 137 yield (
138 138 line_no,
139 139 commit_id,
140 140 lambda: self.repository.get_commit(commit_id=commit_id),
141 141 content)
142 142
143 def get_node(self, path):
143 def get_node(self, path, pre_load=None):
144 144 path = self._fix_path(path)
145 145 if path not in self.nodes:
146 146
147 147 if path == '':
148 148 node = nodes.RootNode(commit=self)
149 149 else:
150 150 node_type = self._remote.get_node_type(
151 151 safe_str(path), self._svn_rev)
152 152 if node_type == 'dir':
153 153 node = nodes.DirNode(path, commit=self)
154 154 elif node_type == 'file':
155 node = nodes.FileNode(path, commit=self)
155 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
156 156 else:
157 157 raise NodeDoesNotExistError(self.no_node_at_path(path))
158 158
159 159 self.nodes[path] = node
160 160 return self.nodes[path]
161 161
162 162 def get_nodes(self, path):
163 163 if self._get_kind(path) != nodes.NodeKind.DIR:
164 164 raise CommitError(
165 165 "Directory does not exist for commit %s at "
166 166 " '%s'" % (self.raw_id, path))
167 167 path = self._fix_path(path)
168 168
169 169 path_nodes = []
170 170 for name, kind in self._remote.get_nodes(
171 171 safe_str(path), revision=self._svn_rev):
172 172 node_path = vcspath.join(path, name)
173 173 if kind == 'dir':
174 174 node = nodes.DirNode(node_path, commit=self)
175 175 elif kind == 'file':
176 176 node = nodes.FileNode(node_path, commit=self)
177 177 else:
178 178 raise ValueError("Node kind %s not supported." % (kind, ))
179 179 self.nodes[node_path] = node
180 180 path_nodes.append(node)
181 181
182 182 return path_nodes
183 183
184 184 def _get_kind(self, path):
185 185 path = self._fix_path(path)
186 186 kind = self._remote.get_node_type(path, self._svn_rev)
187 187 if kind == 'file':
188 188 return nodes.NodeKind.FILE
189 189 elif kind == 'dir':
190 190 return nodes.NodeKind.DIR
191 191 else:
192 192 raise CommitError(
193 193 "Node does not exist at the given path '%s'" % (path, ))
194 194
195 195 @LazyProperty
196 196 def _changes_cache(self):
197 197 return self._remote.revision_changes(self._svn_rev)
198 198
199 199 @LazyProperty
200 200 def affected_files(self):
201 201 changed_files = set()
202 202 for files in self._changes_cache.itervalues():
203 203 changed_files.update(files)
204 204 return list(changed_files)
205 205
206 206 @LazyProperty
207 207 def id(self):
208 208 return self.raw_id
209 209
210 210 @property
211 211 def added(self):
212 212 return nodes.AddedFileNodesGenerator(
213 213 self._changes_cache['added'], self)
214 214
215 215 @property
216 216 def changed(self):
217 217 return nodes.ChangedFileNodesGenerator(
218 218 self._changes_cache['changed'], self)
219 219
220 220 @property
221 221 def removed(self):
222 222 return nodes.RemovedFileNodesGenerator(
223 223 self._changes_cache['removed'], self)
224 224
225 225
226 226 def _date_from_svn_properties(properties):
227 227 """
228 228 Parses the date out of given svn properties.
229 229
230 230 :return: :class:`datetime.datetime` instance. The object is naive.
231 231 """
232 232
233 233 aware_date = dateutil.parser.parse(properties.get('svn:date'))
234 234 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
235 235 final_date = aware_date
236 236 return final_date.replace(tzinfo=None)
@@ -1,756 +1,773 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 23 """
24 24
25 25
26 26 import stat
27 27
28 28 from zope.cachedescriptors.property import Lazy as LazyProperty
29 29
30 30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 31 from rhodecode.lib.utils import safe_unicode, safe_str
32 32 from rhodecode.lib.utils2 import md5
33 33 from rhodecode.lib.vcs import path as vcspath
34 34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37 37
38 38 LARGEFILE_PREFIX = '.hglf'
39 39
40 40
41 41 class NodeKind:
42 42 SUBMODULE = -1
43 43 DIR = 1
44 44 FILE = 2
45 45 LARGEFILE = 3
46 46
47 47
48 48 class NodeState:
49 49 ADDED = u'added'
50 50 CHANGED = u'changed'
51 51 NOT_CHANGED = u'not changed'
52 52 REMOVED = u'removed'
53 53
54 54
55 55 class NodeGeneratorBase(object):
56 56 """
57 57 Base class for removed added and changed filenodes, it's a lazy generator
58 58 class that will create filenodes only on iteration or call
59 59
60 60 The len method doesn't need to create filenodes at all
61 61 """
62 62
63 63 def __init__(self, current_paths, cs):
64 64 self.cs = cs
65 65 self.current_paths = current_paths
66 66
67 67 def __call__(self):
68 68 return [n for n in self]
69 69
70 70 def __getslice__(self, i, j):
71 71 for p in self.current_paths[i:j]:
72 72 yield self.cs.get_node(p)
73 73
74 74 def __len__(self):
75 75 return len(self.current_paths)
76 76
77 77 def __iter__(self):
78 78 for p in self.current_paths:
79 79 yield self.cs.get_node(p)
80 80
81 81
82 82 class AddedFileNodesGenerator(NodeGeneratorBase):
83 83 """
84 84 Class holding added files for current commit
85 85 """
86 86
87 87
88 88 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 89 """
90 90 Class holding changed files for current commit
91 91 """
92 92
93 93
94 94 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 95 """
96 96 Class holding removed files for current commit
97 97 """
98 98 def __iter__(self):
99 99 for p in self.current_paths:
100 100 yield RemovedFileNode(path=p)
101 101
102 102 def __getslice__(self, i, j):
103 103 for p in self.current_paths[i:j]:
104 104 yield RemovedFileNode(path=p)
105 105
106 106
107 107 class Node(object):
108 108 """
109 109 Simplest class representing file or directory on repository. SCM backends
110 110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 111 directly.
112 112
113 113 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 114 only. Moreover, every single node is identified by the ``path`` attribute,
115 115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 116 """
117 117
118 118 commit = None
119 119
120 120 def __init__(self, path, kind):
121 121 self._validate_path(path) # can throw exception if path is invalid
122 122 self.path = safe_str(path.rstrip('/')) # we store paths as str
123 123 if path == '' and kind != NodeKind.DIR:
124 124 raise NodeError("Only DirNode and its subclasses may be "
125 125 "initialized with empty path")
126 126 self.kind = kind
127 127
128 128 if self.is_root() and not self.is_dir():
129 129 raise NodeError("Root node cannot be FILE kind")
130 130
131 131 def _validate_path(self, path):
132 132 if path.startswith('/'):
133 133 raise NodeError(
134 134 "Cannot initialize Node objects with slash at "
135 135 "the beginning as only relative paths are supported. "
136 136 "Got %s" % (path,))
137 137
138 138 @LazyProperty
139 139 def parent(self):
140 140 parent_path = self.get_parent_path()
141 141 if parent_path:
142 142 if self.commit:
143 143 return self.commit.get_node(parent_path)
144 144 return DirNode(parent_path)
145 145 return None
146 146
147 147 @LazyProperty
148 148 def unicode_path(self):
149 149 return safe_unicode(self.path)
150 150
151 151 @LazyProperty
152 152 def dir_path(self):
153 153 """
154 154 Returns name of the directory from full path of this vcs node. Empty
155 155 string is returned if there's no directory in the path
156 156 """
157 157 _parts = self.path.rstrip('/').rsplit('/', 1)
158 158 if len(_parts) == 2:
159 159 return safe_unicode(_parts[0])
160 160 return u''
161 161
162 162 @LazyProperty
163 163 def name(self):
164 164 """
165 165 Returns name of the node so if its path
166 166 then only last part is returned.
167 167 """
168 168 return safe_unicode(self.path.rstrip('/').split('/')[-1])
169 169
170 170 @property
171 171 def kind(self):
172 172 return self._kind
173 173
174 174 @kind.setter
175 175 def kind(self, kind):
176 176 if hasattr(self, '_kind'):
177 177 raise NodeError("Cannot change node's kind")
178 178 else:
179 179 self._kind = kind
180 180 # Post setter check (path's trailing slash)
181 181 if self.path.endswith('/'):
182 182 raise NodeError("Node's path cannot end with slash")
183 183
184 184 def __cmp__(self, other):
185 185 """
186 186 Comparator using name of the node, needed for quick list sorting.
187 187 """
188 188 kind_cmp = cmp(self.kind, other.kind)
189 189 if kind_cmp:
190 190 return kind_cmp
191 191 return cmp(self.name, other.name)
192 192
193 193 def __eq__(self, other):
194 194 for attr in ['name', 'path', 'kind']:
195 195 if getattr(self, attr) != getattr(other, attr):
196 196 return False
197 197 if self.is_file():
198 198 if self.content != other.content:
199 199 return False
200 200 else:
201 201 # For DirNode's check without entering each dir
202 202 self_nodes_paths = list(sorted(n.path for n in self.nodes))
203 203 other_nodes_paths = list(sorted(n.path for n in self.nodes))
204 204 if self_nodes_paths != other_nodes_paths:
205 205 return False
206 206 return True
207 207
208 208 def __ne__(self, other):
209 209 return not self.__eq__(other)
210 210
211 211 def __repr__(self):
212 212 return '<%s %r>' % (self.__class__.__name__, self.path)
213 213
214 214 def __str__(self):
215 215 return self.__repr__()
216 216
217 217 def __unicode__(self):
218 218 return self.name
219 219
220 220 def get_parent_path(self):
221 221 """
222 222 Returns node's parent path or empty string if node is root.
223 223 """
224 224 if self.is_root():
225 225 return ''
226 226 return vcspath.dirname(self.path.rstrip('/')) + '/'
227 227
228 228 def is_file(self):
229 229 """
230 230 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
231 231 otherwise.
232 232 """
233 233 return self.kind == NodeKind.FILE
234 234
235 235 def is_dir(self):
236 236 """
237 237 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
238 238 otherwise.
239 239 """
240 240 return self.kind == NodeKind.DIR
241 241
242 242 def is_root(self):
243 243 """
244 244 Returns ``True`` if node is a root node and ``False`` otherwise.
245 245 """
246 246 return self.kind == NodeKind.DIR and self.path == ''
247 247
248 248 def is_submodule(self):
249 249 """
250 250 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
251 251 otherwise.
252 252 """
253 253 return self.kind == NodeKind.SUBMODULE
254 254
255 255 def is_largefile(self):
256 256 """
257 257 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
258 258 otherwise
259 259 """
260 260 return self.kind == NodeKind.LARGEFILE
261 261
262 262 def is_link(self):
263 263 if self.commit:
264 264 return self.commit.is_link(self.path)
265 265 return False
266 266
267 267 @LazyProperty
268 268 def added(self):
269 269 return self.state is NodeState.ADDED
270 270
271 271 @LazyProperty
272 272 def changed(self):
273 273 return self.state is NodeState.CHANGED
274 274
275 275 @LazyProperty
276 276 def not_changed(self):
277 277 return self.state is NodeState.NOT_CHANGED
278 278
279 279 @LazyProperty
280 280 def removed(self):
281 281 return self.state is NodeState.REMOVED
282 282
283 283
284 284 class FileNode(Node):
285 285 """
286 286 Class representing file nodes.
287 287
288 288 :attribute: path: path to the node, relative to repository's root
289 289 :attribute: content: if given arbitrary sets content of the file
290 290 :attribute: commit: if given, first time content is accessed, callback
291 291 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
292 292 """
293 _filter_pre_load = []
293 294
294 def __init__(self, path, content=None, commit=None, mode=None):
295 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
295 296 """
296 297 Only one of ``content`` and ``commit`` may be given. Passing both
297 298 would raise ``NodeError`` exception.
298 299
299 300 :param path: relative path to the node
300 301 :param content: content may be passed to constructor
301 302 :param commit: if given, will use it to lazily fetch content
302 303 :param mode: ST_MODE (i.e. 0100644)
303 304 """
304 305 if content and commit:
305 306 raise NodeError("Cannot use both content and commit")
306 307 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
307 308 self.commit = commit
308 309 self._content = content
309 310 self._mode = mode or FILEMODE_DEFAULT
310 311
312 self._set_bulk_properties(pre_load)
313
314 def _set_bulk_properties(self, pre_load):
315 if not pre_load:
316 return
317 pre_load = [entry for entry in pre_load
318 if entry not in self._filter_pre_load]
319 if not pre_load:
320 return
321
322 for attr_name in pre_load:
323 result = getattr(self, attr_name)
324 if callable(result):
325 result = result()
326 self.__dict__[attr_name] = result
327
311 328 @LazyProperty
312 329 def mode(self):
313 330 """
314 331 Returns lazily mode of the FileNode. If `commit` is not set, would
315 332 use value given at initialization or `FILEMODE_DEFAULT` (default).
316 333 """
317 334 if self.commit:
318 335 mode = self.commit.get_file_mode(self.path)
319 336 else:
320 337 mode = self._mode
321 338 return mode
322 339
323 340 @LazyProperty
324 341 def raw_bytes(self):
325 342 """
326 343 Returns lazily the raw bytes of the FileNode.
327 344 """
328 345 if self.commit:
329 346 if self._content is None:
330 347 self._content = self.commit.get_file_content(self.path)
331 348 content = self._content
332 349 else:
333 350 content = self._content
334 351 return content
335 352
336 353 @LazyProperty
337 354 def md5(self):
338 355 """
339 356 Returns md5 of the file node.
340 357 """
341 358 return md5(self.raw_bytes)
342 359
343 360 @LazyProperty
344 361 def content(self):
345 362 """
346 363 Returns lazily content of the FileNode. If possible, would try to
347 364 decode content from UTF-8.
348 365 """
349 366 content = self.raw_bytes
350 367
351 368 if self.is_binary:
352 369 return content
353 370 return safe_unicode(content)
354 371
355 372 @LazyProperty
356 373 def size(self):
357 374 if self.commit:
358 375 return self.commit.get_file_size(self.path)
359 376 raise NodeError(
360 377 "Cannot retrieve size of the file without related "
361 378 "commit attribute")
362 379
363 380 @LazyProperty
364 381 def message(self):
365 382 if self.commit:
366 383 return self.last_commit.message
367 384 raise NodeError(
368 385 "Cannot retrieve message of the file without related "
369 386 "commit attribute")
370 387
371 388 @LazyProperty
372 389 def last_commit(self):
373 390 if self.commit:
374 391 pre_load = ["author", "date", "message"]
375 392 return self.commit.get_file_commit(self.path, pre_load=pre_load)
376 393 raise NodeError(
377 394 "Cannot retrieve last commit of the file without "
378 395 "related commit attribute")
379 396
380 397 def get_mimetype(self):
381 398 """
382 399 Mimetype is calculated based on the file's content. If ``_mimetype``
383 400 attribute is available, it will be returned (backends which store
384 401 mimetypes or can easily recognize them, should set this private
385 402 attribute to indicate that type should *NOT* be calculated).
386 403 """
387 404
388 405 if hasattr(self, '_mimetype'):
389 406 if (isinstance(self._mimetype, (tuple, list,)) and
390 407 len(self._mimetype) == 2):
391 408 return self._mimetype
392 409 else:
393 410 raise NodeError('given _mimetype attribute must be an 2 '
394 411 'element list or tuple')
395 412
396 413 db = get_mimetypes_db()
397 414 mtype, encoding = db.guess_type(self.name)
398 415
399 416 if mtype is None:
400 417 if self.is_binary:
401 418 mtype = 'application/octet-stream'
402 419 encoding = None
403 420 else:
404 421 mtype = 'text/plain'
405 422 encoding = None
406 423
407 424 # try with pygments
408 425 try:
409 426 from pygments.lexers import get_lexer_for_filename
410 427 mt = get_lexer_for_filename(self.name).mimetypes
411 428 except Exception:
412 429 mt = None
413 430
414 431 if mt:
415 432 mtype = mt[0]
416 433
417 434 return mtype, encoding
418 435
419 436 @LazyProperty
420 437 def mimetype(self):
421 438 """
422 439 Wrapper around full mimetype info. It returns only type of fetched
423 440 mimetype without the encoding part. use get_mimetype function to fetch
424 441 full set of (type,encoding)
425 442 """
426 443 return self.get_mimetype()[0]
427 444
428 445 @LazyProperty
429 446 def mimetype_main(self):
430 447 return self.mimetype.split('/')[0]
431 448
432 449 @LazyProperty
433 450 def lexer(self):
434 451 """
435 452 Returns pygment's lexer class. Would try to guess lexer taking file's
436 453 content, name and mimetype.
437 454 """
438 455 from pygments import lexers
439 456
440 457 lexer = None
441 458 try:
442 459 lexer = lexers.guess_lexer_for_filename(
443 460 self.name, self.content, stripnl=False)
444 461 except lexers.ClassNotFound:
445 462 lexer = None
446 463
447 464 # try our EXTENSION_MAP
448 465 if not lexer:
449 466 try:
450 467 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(self.extension)
451 468 if lexer_class:
452 469 lexer = lexers.get_lexer_by_name(lexer_class[0])
453 470 except lexers.ClassNotFound:
454 471 lexer = None
455 472
456 473 if not lexer:
457 474 lexer = lexers.TextLexer(stripnl=False)
458 475
459 476 return lexer
460 477
461 478 @LazyProperty
462 479 def lexer_alias(self):
463 480 """
464 481 Returns first alias of the lexer guessed for this file.
465 482 """
466 483 return self.lexer.aliases[0]
467 484
468 485 @LazyProperty
469 486 def history(self):
470 487 """
471 488 Returns a list of commit for this file in which the file was changed
472 489 """
473 490 if self.commit is None:
474 491 raise NodeError('Unable to get commit for this FileNode')
475 492 return self.commit.get_file_history(self.path)
476 493
477 494 @LazyProperty
478 495 def annotate(self):
479 496 """
480 497 Returns a list of three element tuples with lineno, commit and line
481 498 """
482 499 if self.commit is None:
483 500 raise NodeError('Unable to get commit for this FileNode')
484 501 pre_load = ["author", "date", "message"]
485 502 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
486 503
487 504 @LazyProperty
488 505 def state(self):
489 506 if not self.commit:
490 507 raise NodeError(
491 508 "Cannot check state of the node if it's not "
492 509 "linked with commit")
493 510 elif self.path in (node.path for node in self.commit.added):
494 511 return NodeState.ADDED
495 512 elif self.path in (node.path for node in self.commit.changed):
496 513 return NodeState.CHANGED
497 514 else:
498 515 return NodeState.NOT_CHANGED
499 516
500 517 @LazyProperty
501 518 def is_binary(self):
502 519 """
503 520 Returns True if file has binary content.
504 521 """
505 522 _bin = self.raw_bytes and '\0' in self.raw_bytes
506 523 return _bin
507 524
508 525 @LazyProperty
509 526 def extension(self):
510 527 """Returns filenode extension"""
511 528 return self.name.split('.')[-1]
512 529
513 530 @property
514 531 def is_executable(self):
515 532 """
516 533 Returns ``True`` if file has executable flag turned on.
517 534 """
518 535 return bool(self.mode & stat.S_IXUSR)
519 536
520 537 def get_largefile_node(self):
521 538 """
522 539 Try to return a Mercurial FileNode from this node. It does internal
523 540 checks inside largefile store, if that file exist there it will
524 541 create special instance of LargeFileNode which can get content from
525 542 LF store.
526 543 """
527 544 if self.commit and self.path.startswith(LARGEFILE_PREFIX):
528 545 largefile_path = self.path.split(LARGEFILE_PREFIX)[-1].lstrip('/')
529 546 return self.commit.get_largefile_node(largefile_path)
530 547
531 548 def lines(self, count_empty=False):
532 549 all_lines, empty_lines = 0, 0
533 550
534 551 if not self.is_binary:
535 552 content = self.content
536 553 if count_empty:
537 554 all_lines = 0
538 555 empty_lines = 0
539 556 for line in content.splitlines(True):
540 557 if line == '\n':
541 558 empty_lines += 1
542 559 all_lines += 1
543 560
544 561 return all_lines, all_lines - empty_lines
545 562 else:
546 563 # fast method
547 564 empty_lines = all_lines = content.count('\n')
548 565 if all_lines == 0 and content:
549 566 # one-line without a newline
550 567 empty_lines = all_lines = 1
551 568
552 569 return all_lines, empty_lines
553 570
554 571 def __repr__(self):
555 572 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
556 573 getattr(self.commit, 'short_id', ''))
557 574
558 575
559 576 class RemovedFileNode(FileNode):
560 577 """
561 578 Dummy FileNode class - trying to access any public attribute except path,
562 579 name, kind or state (or methods/attributes checking those two) would raise
563 580 RemovedFileNodeError.
564 581 """
565 582 ALLOWED_ATTRIBUTES = [
566 583 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
567 584 'added', 'changed', 'not_changed', 'removed'
568 585 ]
569 586
570 587 def __init__(self, path):
571 588 """
572 589 :param path: relative path to the node
573 590 """
574 591 super(RemovedFileNode, self).__init__(path=path)
575 592
576 593 def __getattribute__(self, attr):
577 594 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
578 595 return super(RemovedFileNode, self).__getattribute__(attr)
579 596 raise RemovedFileNodeError(
580 597 "Cannot access attribute %s on RemovedFileNode" % attr)
581 598
582 599 @LazyProperty
583 600 def state(self):
584 601 return NodeState.REMOVED
585 602
586 603
587 604 class DirNode(Node):
588 605 """
589 606 DirNode stores list of files and directories within this node.
590 607 Nodes may be used standalone but within repository context they
591 608 lazily fetch data within same repositorty's commit.
592 609 """
593 610
594 611 def __init__(self, path, nodes=(), commit=None):
595 612 """
596 613 Only one of ``nodes`` and ``commit`` may be given. Passing both
597 614 would raise ``NodeError`` exception.
598 615
599 616 :param path: relative path to the node
600 617 :param nodes: content may be passed to constructor
601 618 :param commit: if given, will use it to lazily fetch content
602 619 """
603 620 if nodes and commit:
604 621 raise NodeError("Cannot use both nodes and commit")
605 622 super(DirNode, self).__init__(path, NodeKind.DIR)
606 623 self.commit = commit
607 624 self._nodes = nodes
608 625
609 626 @LazyProperty
610 627 def content(self):
611 628 raise NodeError(
612 629 "%s represents a dir and has no `content` attribute" % self)
613 630
614 631 @LazyProperty
615 632 def nodes(self):
616 633 if self.commit:
617 634 nodes = self.commit.get_nodes(self.path)
618 635 else:
619 636 nodes = self._nodes
620 637 self._nodes_dict = dict((node.path, node) for node in nodes)
621 638 return sorted(nodes)
622 639
623 640 @LazyProperty
624 641 def files(self):
625 642 return sorted((node for node in self.nodes if node.is_file()))
626 643
627 644 @LazyProperty
628 645 def dirs(self):
629 646 return sorted((node for node in self.nodes if node.is_dir()))
630 647
631 648 def __iter__(self):
632 649 for node in self.nodes:
633 650 yield node
634 651
635 652 def get_node(self, path):
636 653 """
637 654 Returns node from within this particular ``DirNode``, so it is now
638 655 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
639 656 'docs'. In order to access deeper nodes one must fetch nodes between
640 657 them first - this would work::
641 658
642 659 docs = root.get_node('docs')
643 660 docs.get_node('api').get_node('index.rst')
644 661
645 662 :param: path - relative to the current node
646 663
647 664 .. note::
648 665 To access lazily (as in example above) node have to be initialized
649 666 with related commit object - without it node is out of
650 667 context and may know nothing about anything else than nearest
651 668 (located at same level) nodes.
652 669 """
653 670 try:
654 671 path = path.rstrip('/')
655 672 if path == '':
656 673 raise NodeError("Cannot retrieve node without path")
657 674 self.nodes # access nodes first in order to set _nodes_dict
658 675 paths = path.split('/')
659 676 if len(paths) == 1:
660 677 if not self.is_root():
661 678 path = '/'.join((self.path, paths[0]))
662 679 else:
663 680 path = paths[0]
664 681 return self._nodes_dict[path]
665 682 elif len(paths) > 1:
666 683 if self.commit is None:
667 684 raise NodeError(
668 685 "Cannot access deeper nodes without commit")
669 686 else:
670 687 path1, path2 = paths[0], '/'.join(paths[1:])
671 688 return self.get_node(path1).get_node(path2)
672 689 else:
673 690 raise KeyError
674 691 except KeyError:
675 692 raise NodeError("Node does not exist at %s" % path)
676 693
677 694 @LazyProperty
678 695 def state(self):
679 696 raise NodeError("Cannot access state of DirNode")
680 697
681 698 @LazyProperty
682 699 def size(self):
683 700 size = 0
684 701 for root, dirs, files in self.commit.walk(self.path):
685 702 for f in files:
686 703 size += f.size
687 704
688 705 return size
689 706
690 707 def __repr__(self):
691 708 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
692 709 getattr(self.commit, 'short_id', ''))
693 710
694 711
695 712 class RootNode(DirNode):
696 713 """
697 714 DirNode being the root node of the repository.
698 715 """
699 716
700 717 def __init__(self, nodes=(), commit=None):
701 718 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
702 719
703 720 def __repr__(self):
704 721 return '<%s>' % self.__class__.__name__
705 722
706 723
707 724 class SubModuleNode(Node):
708 725 """
709 726 represents a SubModule of Git or SubRepo of Mercurial
710 727 """
711 728 is_binary = False
712 729 size = 0
713 730
714 731 def __init__(self, name, url=None, commit=None, alias=None):
715 732 self.path = name
716 733 self.kind = NodeKind.SUBMODULE
717 734 self.alias = alias
718 735
719 736 # we have to use EmptyCommit here since this can point to svn/git/hg
720 737 # submodules we cannot get from repository
721 738 self.commit = EmptyCommit(str(commit), alias=alias)
722 739 self.url = url or self._extract_submodule_url()
723 740
724 741 def __repr__(self):
725 742 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
726 743 getattr(self.commit, 'short_id', ''))
727 744
728 745 def _extract_submodule_url(self):
729 746 # TODO: find a way to parse gits submodule file and extract the
730 747 # linking URL
731 748 return self.path
732 749
733 750 @LazyProperty
734 751 def name(self):
735 752 """
736 753 Returns name of the node so if its path
737 754 then only last part is returned.
738 755 """
739 756 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
740 757 return u'%s @ %s' % (org, self.commit.short_id)
741 758
742 759
743 760 class LargeFileNode(FileNode):
744 761
745 762 def _validate_path(self, path):
746 763 """
747 764 we override check since the LargeFileNode path is system absolute
748 765 """
749 766
750 767 def raw_bytes(self):
751 768 if self.commit:
752 769 with open(self.path, 'rb') as f:
753 770 content = f.read()
754 771 else:
755 772 content = self._content
756 773 return content No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now