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