@@ -140,253 +140,253 @@ class MercurialChangeset(BaseChangeset):
Returns list of parents changesets.
"""
return [self.repository.get_changeset(parent.rev())
for parent in self._ctx.parents() if parent.rev() >= 0]
@LazyProperty
def children(self):
Returns list of children changesets.
return [self.repository.get_changeset(child.rev())
for child in self._ctx.children() if child.rev() >= 0]
def next(self, branch=None):
if branch and self.branch != branch:
raise VCSError('Branch option used on changeset not belonging '
'to that branch')
cs = self
while True:
try:
next_ = cs.repository.revisions.index(cs.raw_id) + 1
next_rev = cs.repository.revisions[next_]
except IndexError:
raise ChangesetDoesNotExistError
cs = cs.repository.get_changeset(next_rev)
if not branch or branch == cs.branch:
return cs
def prev(self, branch=None):
prev_ = cs.repository.revisions.index(cs.raw_id) - 1
if prev_ < 0:
raise IndexError
prev_rev = cs.repository.revisions[prev_]
cs = cs.repository.get_changeset(prev_rev)
def diff(self):
# Only used to feed diffstat
return b''.join(self._ctx.diff())
def _get_kind(self, path):
path = path.rstrip('/')
if path in self._file_paths:
return NodeKind.FILE
elif path in self._dir_paths:
return NodeKind.DIR
else:
raise ChangesetError("Node does not exist at the given path '%s'"
% (path))
def _get_filectx(self, path):
if self._get_kind(path) != NodeKind.FILE:
raise ChangesetError("File does not exist for revision %s at "
" '%s'" % (self.raw_id, path))
return self._ctx.filectx(safe_bytes(path))
def _extract_submodules(self):
returns a dictionary with submodule information from substate file
of hg repository
return self._ctx.substate
def get_file_mode(self, path):
Returns stat mode of the file at the given ``path``.
fctx = self._get_filectx(path)
if b'x' in fctx.flags():
return 0o100755
return 0o100644
def get_file_content(self, path):
Returns content of the file at given ``path``.
return fctx.data()
def get_file_size(self, path):
Returns size of the file at given ``path``.
return fctx.size()
def get_file_changeset(self, path):
Returns last commit of the file at the given ``path``.
return self.get_file_history(path, limit=1)[0]
def get_file_history(self, path, limit=None):
Returns history of file as reversed list of ``Changeset`` objects for
which file at given ``path`` has been modified.
hist = []
cnt = 0
for cs in reversed([x for x in fctx.filelog()]):
cnt += 1
hist.append(mercurial.node.hex(fctx.filectx(cs).node()))
if limit is not None and cnt == limit:
break
return [self.repository.get_changeset(node) for node in hist]
def get_file_annotate(self, path):
Returns a generator of four element tuples with
lineno, sha, changeset lazy loader and line
annotations = self._get_filectx(path).annotate()
annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations]
for i, (fctx, line) in enumerate(annotation_lines):
sha = ascii_str(fctx.hex())
yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line)
def fill_archive(self, stream=None, kind='tgz', prefix=None,
subrepos=False):
Fills up given stream.
:param stream: file like object.
:param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
Default: ``tgz``.
:param prefix: name of root directory in archive.
Default is repository name and changeset's raw_id joined with dash
(``repo-tip.<KIND>``).
:param subrepos: include subrepos in this archive.
:raise ImproperArchiveTypeError: If given kind is wrong.
:raise VcsError: If given stream is None
allowed_kinds = settings.ARCHIVE_SPECS
if kind not in allowed_kinds:
raise ImproperArchiveTypeError('Archive kind not supported use one'
'of %s' % ' '.join(allowed_kinds))
if stream is None:
raise VCSError('You need to pass in a valid stream for filling'
' with archival data')
if prefix is None:
prefix = '%s-%s' % (self.repository.name, self.short_id)
elif prefix.startswith('/'):
raise VCSError("Prefix cannot start with leading slash")
elif prefix.strip() == '':
raise VCSError("Prefix cannot be empty")
mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id),
safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos)
def get_nodes(self, path):
Returns combined ``DirNode`` and ``FileNode`` objects list representing
state of changeset at the given ``path``. If node at the given ``path``
is not instance of ``DirNode``, ChangesetError would be raised.
if self._get_kind(path) != NodeKind.DIR:
raise ChangesetError("Directory does not exist for revision %s at "
" '%s'" % (self.revision, path))
filenodes = [FileNode(f, changeset=self) for f in self._file_paths
if os.path.dirname(f) == path]
dirs = path == '' and '' or [d for d in self._dir_paths
if d and posixpath.dirname(d) == path]
dirnodes = [DirNode(d, changeset=self) for d in dirs
if os.path.dirname(d) == path]
als = self.repository.alias
for k, vals in self._extract_submodules().items():
#vals = url,rev,type
loc = vals[0]
cs = vals[1]
dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
dirnodes.append(SubModuleNode(safe_str(k), url=safe_str(loc), changeset=cs,
alias=als))
nodes = dirnodes + filenodes
for node in nodes:
self.nodes[node.path] = node
nodes.sort()
return nodes
def get_node(self, path):
Returns ``Node`` object from the given ``path``. If there is no node at
the given ``path``, ``ChangesetError`` would be raised.
if path not in self.nodes:
node = FileNode(path, changeset=self)
elif path in self._dir_paths or path in self._dir_paths:
if path == '':
node = RootNode(changeset=self)
node = DirNode(path, changeset=self)
raise NodeDoesNotExistError("There is no file nor directory "
"at the given path: '%s' at revision %s"
% (path, self.short_id))
# cache node
self.nodes[path] = node
return self.nodes[path]
def affected_files(self):
Gets a fast accessible file changes for given changeset
return self._ctx.files()
@property
def added(self):
Returns list of added ``FileNode`` objects.
return AddedFileNodesGenerator([safe_str(n) for n in self.status.added], self)
def changed(self):
Returns list of modified ``FileNode`` objects.
return ChangedFileNodesGenerator([safe_str(n) for n in self.status.modified], self)
def removed(self):
Returns list of removed ``FileNode`` objects.
return RemovedFileNodesGenerator([safe_str(n) for n in self.status.removed], self)
def extra(self):
return self._ctx.extra()
@@ -414,193 +414,193 @@ class FileNode(Node):
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)
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 repository'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 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
def __eq__(self, other):
eq = super(DirNode, self).__eq__(other)
if eq is not None:
return eq
# 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))
return self_nodes_paths == other_nodes_paths
def __lt__(self, other):
lt = super(DirNode, self).__lt__(other)
if lt is not None:
return lt
return self_nodes_paths < other_nodes_paths
def nodes(self):
if self.changeset:
nodes = self.changeset.get_nodes(self.path)
nodes = self._nodes
self._nodes_dict = dict((node.path, node) for node in nodes)
return sorted(nodes)
def files(self):
return sorted((node for node in self.nodes if node.is_file()))
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
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.
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]))
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")
path1, path2 = paths[0], '/'.join(paths[1:])
return self.get_node(path1).get_node(path2)
raise KeyError
except KeyError:
raise NodeError("Node does not exist at %s" % path)
raise NodeError("Cannot access state of DirNode")
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)
return '<%s>' % self.__class__.__name__
class SubModuleNode(Node):
represents a SubModule of Git or SubRepo of Mercurial
is_binary = False
def __init__(self, name, url, changeset=None, alias=None):
# Note: Doesn't call Node.__init__!
self.path = name.rstrip('/')
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(changeset, alias=alias)
self.url = url
def name(self):
Returns name of the node so if its path
then only last part is returned.
org = self.path.rstrip('/').rsplit('/', 1)[-1]
return '%s @ %s' % (org, self.changeset.short_id)
return '%s @ %s' % (org, safe_str(self.changeset.short_id))
Status change: