|
|
# -*- coding: utf-8 -*-
|
|
|
"""
|
|
|
vcs.nodes
|
|
|
~~~~~~~~~
|
|
|
|
|
|
Module holding everything related to vcs nodes.
|
|
|
|
|
|
:created_on: Apr 8, 2010
|
|
|
:copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
|
|
|
"""
|
|
|
import os
|
|
|
import stat
|
|
|
import posixpath
|
|
|
import mimetypes
|
|
|
|
|
|
from pygments import lexers
|
|
|
|
|
|
from rhodecode.lib.vcs.utils.lazy import LazyProperty
|
|
|
from rhodecode.lib.vcs.utils import safe_unicode
|
|
|
from rhodecode.lib.vcs.exceptions import NodeError
|
|
|
from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
|
|
|
from rhodecode.lib.vcs.backends.base import EmptyChangeset
|
|
|
|
|
|
|
|
|
class NodeKind:
|
|
|
SUBMODULE = -1
|
|
|
DIR = 1
|
|
|
FILE = 2
|
|
|
|
|
|
|
|
|
class NodeState:
|
|
|
ADDED = u'added'
|
|
|
CHANGED = u'changed'
|
|
|
NOT_CHANGED = u'not changed'
|
|
|
REMOVED = u'removed'
|
|
|
|
|
|
|
|
|
class NodeGeneratorBase(object):
|
|
|
"""
|
|
|
Base class for removed added and changed filenodes, it's a lazy generator
|
|
|
class that will create filenodes only on iteration or call
|
|
|
|
|
|
The len method doesn't need to create filenodes at all
|
|
|
"""
|
|
|
|
|
|
def __init__(self, current_paths, cs):
|
|
|
self.cs = cs
|
|
|
self.current_paths = current_paths
|
|
|
|
|
|
def __call__(self):
|
|
|
return [n for n in self]
|
|
|
|
|
|
def __getslice__(self, i, j):
|
|
|
for p in self.current_paths[i:j]:
|
|
|
yield self.cs.get_node(p)
|
|
|
|
|
|
def __len__(self):
|
|
|
return len(self.current_paths)
|
|
|
|
|
|
def __iter__(self):
|
|
|
for p in self.current_paths:
|
|
|
yield self.cs.get_node(p)
|
|
|
|
|
|
|
|
|
class AddedFileNodesGenerator(NodeGeneratorBase):
|
|
|
"""
|
|
|
Class holding Added files for current changeset
|
|
|
"""
|
|
|
pass
|
|
|
|
|
|
|
|
|
class ChangedFileNodesGenerator(NodeGeneratorBase):
|
|
|
"""
|
|
|
Class holding Changed files for current changeset
|
|
|
"""
|
|
|
pass
|
|
|
|
|
|
|
|
|
class RemovedFileNodesGenerator(NodeGeneratorBase):
|
|
|
"""
|
|
|
Class holding removed files for current changeset
|
|
|
"""
|
|
|
def __iter__(self):
|
|
|
for p in self.current_paths:
|
|
|
yield RemovedFileNode(path=p)
|
|
|
|
|
|
def __getslice__(self, i, j):
|
|
|
for p in self.current_paths[i:j]:
|
|
|
yield RemovedFileNode(path=p)
|
|
|
|
|
|
|
|
|
class Node(object):
|
|
|
"""
|
|
|
Simplest class representing file or directory on repository. SCM backends
|
|
|
should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
|
|
|
directly.
|
|
|
|
|
|
Node's ``path`` cannot start with slash as we operate on *relative* paths
|
|
|
only. Moreover, every single node is identified by the ``path`` attribute,
|
|
|
so it cannot end with slash, too. Otherwise, path could lead to mistakes.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, path, kind):
|
|
|
if path.startswith('/'):
|
|
|
raise NodeError("Cannot initialize Node objects with slash at "
|
|
|
"the beginning as only relative paths are supported")
|
|
|
self.path = path.rstrip('/')
|
|
|
if path == '' and kind != NodeKind.DIR:
|
|
|
raise NodeError("Only DirNode and its subclasses may be "
|
|
|
"initialized with empty path")
|
|
|
self.kind = kind
|
|
|
#self.dirs, self.files = [], []
|
|
|
if self.is_root() and not self.is_dir():
|
|
|
raise NodeError("Root node cannot be FILE kind")
|
|
|
|
|
|
@LazyProperty
|
|
|
def parent(self):
|
|
|
parent_path = self.get_parent_path()
|
|
|
if parent_path:
|
|
|
if self.changeset:
|
|
|
return self.changeset.get_node(parent_path)
|
|
|
return DirNode(parent_path)
|
|
|
return None
|
|
|
|
|
|
@LazyProperty
|
|
|
def unicode_path(self):
|
|
|
return safe_unicode(self.path)
|
|
|
|
|
|
@LazyProperty
|
|
|
def name(self):
|
|
|
"""
|
|
|
Returns name of the node so if its path
|
|
|
then only last part is returned.
|
|
|
"""
|
|
|
return safe_unicode(self.path.rstrip('/').split('/')[-1])
|
|
|
|
|
|
def _get_kind(self):
|
|
|
return self._kind
|
|
|
|
|
|
def _set_kind(self, kind):
|
|
|
if hasattr(self, '_kind'):
|
|
|
raise NodeError("Cannot change node's kind")
|
|
|
else:
|
|
|
self._kind = kind
|
|
|
# Post setter check (path's trailing slash)
|
|
|
if self.path.endswith('/'):
|
|
|
raise NodeError("Node's path cannot end with slash")
|
|
|
|
|
|
kind = property(_get_kind, _set_kind)
|
|
|
|
|
|
def __cmp__(self, other):
|
|
|
"""
|
|
|
Comparator using name of the node, needed for quick list sorting.
|
|
|
"""
|
|
|
kind_cmp = cmp(self.kind, other.kind)
|
|
|
if kind_cmp:
|
|
|
return kind_cmp
|
|
|
return cmp(self.name, other.name)
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
for attr in ['name', 'path', 'kind']:
|
|
|
if getattr(self, attr) != getattr(other, attr):
|
|
|
return False
|
|
|
if self.is_file():
|
|
|
if self.content != other.content:
|
|
|
return False
|
|
|
else:
|
|
|
# For DirNode's check without entering each dir
|
|
|
self_nodes_paths = list(sorted(n.path for n in self.nodes))
|
|
|
other_nodes_paths = list(sorted(n.path for n in self.nodes))
|
|
|
if self_nodes_paths != other_nodes_paths:
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
def __nq__(self, other):
|
|
|
return not self.__eq__(other)
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<%s %r>' % (self.__class__.__name__, self.path)
|
|
|
|
|
|
def __str__(self):
|
|
|
return self.__repr__()
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.name
|
|
|
|
|
|
def get_parent_path(self):
|
|
|
"""
|
|
|
Returns node's parent path or empty string if node is root.
|
|
|
"""
|
|
|
if self.is_root():
|
|
|
return ''
|
|
|
return posixpath.dirname(self.path.rstrip('/')) + '/'
|
|
|
|
|
|
def is_file(self):
|
|
|
"""
|
|
|
Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
|
|
|
otherwise.
|
|
|
"""
|
|
|
return self.kind == NodeKind.FILE
|
|
|
|
|
|
def is_dir(self):
|
|
|
"""
|
|
|
Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
|
|
|
otherwise.
|
|
|
"""
|
|
|
return self.kind == NodeKind.DIR
|
|
|
|
|
|
def is_root(self):
|
|
|
"""
|
|
|
Returns ``True`` if node is a root node and ``False`` otherwise.
|
|
|
"""
|
|
|
return self.kind == NodeKind.DIR and self.path == ''
|
|
|
|
|
|
def is_submodule(self):
|
|
|
"""
|
|
|
Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
|
|
|
otherwise.
|
|
|
"""
|
|
|
return self.kind == NodeKind.SUBMODULE
|
|
|
|
|
|
@LazyProperty
|
|
|
def added(self):
|
|
|
return self.state is NodeState.ADDED
|
|
|
|
|
|
@LazyProperty
|
|
|
def changed(self):
|
|
|
return self.state is NodeState.CHANGED
|
|
|
|
|
|
@LazyProperty
|
|
|
def not_changed(self):
|
|
|
return self.state is NodeState.NOT_CHANGED
|
|
|
|
|
|
@LazyProperty
|
|
|
def removed(self):
|
|
|
return self.state is NodeState.REMOVED
|
|
|
|
|
|
|
|
|
class FileNode(Node):
|
|
|
"""
|
|
|
Class representing file nodes.
|
|
|
|
|
|
:attribute: path: path to the node, relative to repostiory's root
|
|
|
:attribute: content: if given arbitrary sets content of the file
|
|
|
:attribute: changeset: if given, first time content is accessed, callback
|
|
|
:attribute: mode: octal stat mode for a node. Default is 0100644.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, path, content=None, changeset=None, mode=None):
|
|
|
"""
|
|
|
Only one of ``content`` and ``changeset`` may be given. Passing both
|
|
|
would raise ``NodeError`` exception.
|
|
|
|
|
|
:param path: relative path to the node
|
|
|
:param content: content may be passed to constructor
|
|
|
:param changeset: if given, will use it to lazily fetch content
|
|
|
:param mode: octal representation of ST_MODE (i.e. 0100644)
|
|
|
"""
|
|
|
|
|
|
if content and changeset:
|
|
|
raise NodeError("Cannot use both content and changeset")
|
|
|
super(FileNode, self).__init__(path, kind=NodeKind.FILE)
|
|
|
self.changeset = changeset
|
|
|
self._content = content
|
|
|
self._mode = mode or 0100644
|
|
|
|
|
|
@LazyProperty
|
|
|
def mode(self):
|
|
|
"""
|
|
|
Returns lazily mode of the FileNode. If ``changeset`` is not set, would
|
|
|
use value given at initialization or 0100644 (default).
|
|
|
"""
|
|
|
if self.changeset:
|
|
|
mode = self.changeset.get_file_mode(self.path)
|
|
|
else:
|
|
|
mode = self._mode
|
|
|
return mode
|
|
|
|
|
|
def _get_content(self):
|
|
|
if self.changeset:
|
|
|
content = self.changeset.get_file_content(self.path)
|
|
|
else:
|
|
|
content = self._content
|
|
|
return content
|
|
|
|
|
|
@property
|
|
|
def content(self):
|
|
|
"""
|
|
|
Returns lazily content of the FileNode. If possible, would try to
|
|
|
decode content from UTF-8.
|
|
|
"""
|
|
|
content = self._get_content()
|
|
|
|
|
|
if bool(content and '\0' in content):
|
|
|
return content
|
|
|
return safe_unicode(content)
|
|
|
|
|
|
@LazyProperty
|
|
|
def size(self):
|
|
|
if self.changeset:
|
|
|
return self.changeset.get_file_size(self.path)
|
|
|
raise NodeError("Cannot retrieve size of the file without related "
|
|
|
"changeset attribute")
|
|
|
|
|
|
@LazyProperty
|
|
|
def message(self):
|
|
|
if self.changeset:
|
|
|
return self.last_changeset.message
|
|
|
raise NodeError("Cannot retrieve message of the file without related "
|
|
|
"changeset attribute")
|
|
|
|
|
|
@LazyProperty
|
|
|
def last_changeset(self):
|
|
|
if self.changeset:
|
|
|
return self.changeset.get_file_changeset(self.path)
|
|
|
raise NodeError("Cannot retrieve last changeset of the file without "
|
|
|
"related changeset attribute")
|
|
|
|
|
|
def get_mimetype(self):
|
|
|
"""
|
|
|
Mimetype is calculated based on the file's content. If ``_mimetype``
|
|
|
attribute is available, it will be returned (backends which store
|
|
|
mimetypes or can easily recognize them, should set this private
|
|
|
attribute to indicate that type should *NOT* be calculated).
|
|
|
"""
|
|
|
if hasattr(self, '_mimetype'):
|
|
|
if (isinstance(self._mimetype, (tuple, list,)) and
|
|
|
len(self._mimetype) == 2):
|
|
|
return self._mimetype
|
|
|
else:
|
|
|
raise NodeError('given _mimetype attribute must be an 2 '
|
|
|
'element list or tuple')
|
|
|
|
|
|
mtype, encoding = mimetypes.guess_type(self.name)
|
|
|
|
|
|
if mtype is None:
|
|
|
if self.is_binary:
|
|
|
mtype = 'application/octet-stream'
|
|
|
encoding = None
|
|
|
else:
|
|
|
mtype = 'text/plain'
|
|
|
encoding = None
|
|
|
return mtype, encoding
|
|
|
|
|
|
@LazyProperty
|
|
|
def mimetype(self):
|
|
|
"""
|
|
|
Wrapper around full mimetype info. It returns only type of fetched
|
|
|
mimetype without the encoding part. use get_mimetype function to fetch
|
|
|
full set of (type,encoding)
|
|
|
"""
|
|
|
return self.get_mimetype()[0]
|
|
|
|
|
|
@LazyProperty
|
|
|
def mimetype_main(self):
|
|
|
return ['', '']
|
|
|
return self.mimetype.split('/')[0]
|
|
|
|
|
|
@LazyProperty
|
|
|
def lexer(self):
|
|
|
"""
|
|
|
Returns pygment's lexer class. Would try to guess lexer taking file's
|
|
|
content, name and mimetype.
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
lexer = lexers.guess_lexer_for_filename(self.name, self.content, stripnl=False)
|
|
|
except lexers.ClassNotFound:
|
|
|
lexer = lexers.TextLexer(stripnl=False)
|
|
|
# returns first alias
|
|
|
return lexer
|
|
|
|
|
|
@LazyProperty
|
|
|
def lexer_alias(self):
|
|
|
"""
|
|
|
Returns first alias of the lexer guessed for this file.
|
|
|
"""
|
|
|
return self.lexer.aliases[0]
|
|
|
|
|
|
@LazyProperty
|
|
|
def history(self):
|
|
|
"""
|
|
|
Returns a list of changeset for this file in which the file was changed
|
|
|
"""
|
|
|
if self.changeset is None:
|
|
|
raise NodeError('Unable to get changeset for this FileNode')
|
|
|
return self.changeset.get_file_history(self.path)
|
|
|
|
|
|
@LazyProperty
|
|
|
def annotate(self):
|
|
|
"""
|
|
|
Returns a list of three element tuples with lineno,changeset and line
|
|
|
"""
|
|
|
if self.changeset is None:
|
|
|
raise NodeError('Unable to get changeset for this FileNode')
|
|
|
return self.changeset.get_file_annotate(self.path)
|
|
|
|
|
|
@LazyProperty
|
|
|
def state(self):
|
|
|
if not self.changeset:
|
|
|
raise NodeError("Cannot check state of the node if it's not "
|
|
|
"linked with changeset")
|
|
|
elif self.path in (node.path for node in self.changeset.added):
|
|
|
return NodeState.ADDED
|
|
|
elif self.path in (node.path for node in self.changeset.changed):
|
|
|
return NodeState.CHANGED
|
|
|
else:
|
|
|
return NodeState.NOT_CHANGED
|
|
|
|
|
|
@property
|
|
|
def is_binary(self):
|
|
|
"""
|
|
|
Returns True if file has binary content.
|
|
|
"""
|
|
|
_bin = '\0' in self._get_content()
|
|
|
return _bin
|
|
|
|
|
|
@LazyProperty
|
|
|
def extension(self):
|
|
|
"""Returns filenode extension"""
|
|
|
return self.name.split('.')[-1]
|
|
|
|
|
|
@property
|
|
|
def is_executable(self):
|
|
|
"""
|
|
|
Returns ``True`` if file has executable flag turned on.
|
|
|
"""
|
|
|
return bool(self.mode & stat.S_IXUSR)
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
|
|
|
getattr(self.changeset, 'short_id', ''))
|
|
|
|
|
|
|
|
|
class RemovedFileNode(FileNode):
|
|
|
"""
|
|
|
Dummy FileNode class - trying to access any public attribute except path,
|
|
|
name, kind or state (or methods/attributes checking those two) would raise
|
|
|
RemovedFileNodeError.
|
|
|
"""
|
|
|
ALLOWED_ATTRIBUTES = [
|
|
|
'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
|
|
|
'added', 'changed', 'not_changed', 'removed'
|
|
|
]
|
|
|
|
|
|
def __init__(self, path):
|
|
|
"""
|
|
|
:param path: relative path to the node
|
|
|
"""
|
|
|
super(RemovedFileNode, self).__init__(path=path)
|
|
|
|
|
|
def __getattribute__(self, attr):
|
|
|
if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
|
|
|
return super(RemovedFileNode, self).__getattribute__(attr)
|
|
|
raise RemovedFileNodeError("Cannot access attribute %s on "
|
|
|
"RemovedFileNode" % attr)
|
|
|
|
|
|
@LazyProperty
|
|
|
def state(self):
|
|
|
return NodeState.REMOVED
|
|
|
|
|
|
|
|
|
class DirNode(Node):
|
|
|
"""
|
|
|
DirNode stores list of files and directories within this node.
|
|
|
Nodes may be used standalone but within repository context they
|
|
|
lazily fetch data within same repositorty's changeset.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, path, nodes=(), changeset=None):
|
|
|
"""
|
|
|
Only one of ``nodes`` and ``changeset`` may be given. Passing both
|
|
|
would raise ``NodeError`` exception.
|
|
|
|
|
|
:param path: relative path to the node
|
|
|
:param nodes: content may be passed to constructor
|
|
|
:param changeset: if given, will use it to lazily fetch content
|
|
|
:param size: always 0 for ``DirNode``
|
|
|
"""
|
|
|
if nodes and changeset:
|
|
|
raise NodeError("Cannot use both nodes and changeset")
|
|
|
super(DirNode, self).__init__(path, NodeKind.DIR)
|
|
|
self.changeset = changeset
|
|
|
self._nodes = nodes
|
|
|
|
|
|
@LazyProperty
|
|
|
def content(self):
|
|
|
raise NodeError("%s represents a dir and has no ``content`` attribute"
|
|
|
% self)
|
|
|
|
|
|
@LazyProperty
|
|
|
def nodes(self):
|
|
|
if self.changeset:
|
|
|
nodes = self.changeset.get_nodes(self.path)
|
|
|
else:
|
|
|
nodes = self._nodes
|
|
|
self._nodes_dict = dict((node.path, node) for node in nodes)
|
|
|
return sorted(nodes)
|
|
|
|
|
|
@LazyProperty
|
|
|
def files(self):
|
|
|
return sorted((node for node in self.nodes if node.is_file()))
|
|
|
|
|
|
@LazyProperty
|
|
|
def dirs(self):
|
|
|
return sorted((node for node in self.nodes if node.is_dir()))
|
|
|
|
|
|
def __iter__(self):
|
|
|
for node in self.nodes:
|
|
|
yield node
|
|
|
|
|
|
def get_node(self, path):
|
|
|
"""
|
|
|
Returns node from within this particular ``DirNode``, so it is now
|
|
|
allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
|
|
|
'docs'. In order to access deeper nodes one must fetch nodes between
|
|
|
them first - this would work::
|
|
|
|
|
|
docs = root.get_node('docs')
|
|
|
docs.get_node('api').get_node('index.rst')
|
|
|
|
|
|
:param: path - relative to the current node
|
|
|
|
|
|
.. note::
|
|
|
To access lazily (as in example above) node have to be initialized
|
|
|
with related changeset object - without it node is out of
|
|
|
context and may know nothing about anything else than nearest
|
|
|
(located at same level) nodes.
|
|
|
"""
|
|
|
try:
|
|
|
path = path.rstrip('/')
|
|
|
if path == '':
|
|
|
raise NodeError("Cannot retrieve node without path")
|
|
|
self.nodes # access nodes first in order to set _nodes_dict
|
|
|
paths = path.split('/')
|
|
|
if len(paths) == 1:
|
|
|
if not self.is_root():
|
|
|
path = '/'.join((self.path, paths[0]))
|
|
|
else:
|
|
|
path = paths[0]
|
|
|
return self._nodes_dict[path]
|
|
|
elif len(paths) > 1:
|
|
|
if self.changeset is None:
|
|
|
raise NodeError("Cannot access deeper "
|
|
|
"nodes without changeset")
|
|
|
else:
|
|
|
path1, path2 = paths[0], '/'.join(paths[1:])
|
|
|
return self.get_node(path1).get_node(path2)
|
|
|
else:
|
|
|
raise KeyError
|
|
|
except KeyError:
|
|
|
raise NodeError("Node does not exist at %s" % path)
|
|
|
|
|
|
@LazyProperty
|
|
|
def state(self):
|
|
|
raise NodeError("Cannot access state of DirNode")
|
|
|
|
|
|
@LazyProperty
|
|
|
def size(self):
|
|
|
size = 0
|
|
|
for root, dirs, files in self.changeset.walk(self.path):
|
|
|
for f in files:
|
|
|
size += f.size
|
|
|
|
|
|
return size
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
|
|
|
getattr(self.changeset, 'short_id', ''))
|
|
|
|
|
|
|
|
|
class RootNode(DirNode):
|
|
|
"""
|
|
|
DirNode being the root node of the repository.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, nodes=(), changeset=None):
|
|
|
super(RootNode, self).__init__(path='', nodes=nodes,
|
|
|
changeset=changeset)
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<%s>' % self.__class__.__name__
|
|
|
|
|
|
|
|
|
class SubModuleNode(Node):
|
|
|
"""
|
|
|
represents a SubModule of Git or SubRepo of Mercurial
|
|
|
"""
|
|
|
is_binary = False
|
|
|
size = 0
|
|
|
|
|
|
def __init__(self, name, url=None, changeset=None, alias=None):
|
|
|
self.path = name
|
|
|
self.kind = NodeKind.SUBMODULE
|
|
|
self.alias = alias
|
|
|
# we have to use emptyChangeset here since this can point to svn/git/hg
|
|
|
# submodules we cannot get from repository
|
|
|
self.changeset = EmptyChangeset(str(changeset), alias=alias)
|
|
|
self.url = url or self._extract_submodule_url()
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
|
|
|
getattr(self.changeset, 'short_id', ''))
|
|
|
|
|
|
def _extract_submodule_url(self):
|
|
|
if self.alias == 'git':
|
|
|
#TODO: find a way to parse gits submodule file and extract the
|
|
|
# linking URL
|
|
|
return self.path
|
|
|
if self.alias == 'hg':
|
|
|
return self.path
|
|
|
|
|
|
@LazyProperty
|
|
|
def name(self):
|
|
|
"""
|
|
|
Returns name of the node so if its path
|
|
|
then only last part is returned.
|
|
|
"""
|
|
|
org = safe_unicode(self.path.rstrip('/').split('/')[-1])
|
|
|
return u'%s @ %s' % (org, self.changeset.short_id)
|
|
|
|