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