changeset.py
357 lines
| 11.2 KiB
| text/x-python
|
PythonLexer
r2007 | import os | |||
import posixpath | ||||
from rhodecode.lib.vcs.backends.base import BaseChangeset | ||||
from rhodecode.lib.vcs.conf import settings | ||||
from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \ | ||||
ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError | ||||
r2232 | from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \ | |||
ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \ | ||||
RemovedFileNodesGenerator, RootNode, SubModuleNode | ||||
r2007 | ||||
from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp | ||||
from rhodecode.lib.vcs.utils.lazy import LazyProperty | ||||
from rhodecode.lib.vcs.utils.paths import get_dirs_for_path | ||||
from ...utils.hgcompat import archival, hex | ||||
class MercurialChangeset(BaseChangeset): | ||||
""" | ||||
Represents state of the repository at the single revision. | ||||
""" | ||||
def __init__(self, repository, revision): | ||||
self.repository = repository | ||||
self.raw_id = revision | ||||
self._ctx = repository._repo[revision] | ||||
self.revision = self._ctx._rev | ||||
self.nodes = {} | ||||
@LazyProperty | ||||
def tags(self): | ||||
return map(safe_unicode, self._ctx.tags()) | ||||
@LazyProperty | ||||
def branch(self): | ||||
return safe_unicode(self._ctx.branch()) | ||||
@LazyProperty | ||||
r2247 | def bookmarks(self): | |||
return map(safe_unicode, self._ctx.bookmarks()) | ||||
@LazyProperty | ||||
r2007 | def message(self): | |||
return safe_unicode(self._ctx.description()) | ||||
@LazyProperty | ||||
def author(self): | ||||
return safe_unicode(self._ctx.user()) | ||||
@LazyProperty | ||||
def date(self): | ||||
return date_fromtimestamp(*self._ctx.date()) | ||||
@LazyProperty | ||||
def status(self): | ||||
""" | ||||
Returns modified, added, removed, deleted files for current changeset | ||||
""" | ||||
return self.repository._repo.status(self._ctx.p1().node(), | ||||
self._ctx.node()) | ||||
@LazyProperty | ||||
def _file_paths(self): | ||||
return list(self._ctx) | ||||
@LazyProperty | ||||
def _dir_paths(self): | ||||
p = list(set(get_dirs_for_path(*self._file_paths))) | ||||
p.insert(0, '') | ||||
return p | ||||
@LazyProperty | ||||
def _paths(self): | ||||
return self._dir_paths + self._file_paths | ||||
@LazyProperty | ||||
def id(self): | ||||
if self.last: | ||||
return u'tip' | ||||
return self.short_id | ||||
@LazyProperty | ||||
def short_id(self): | ||||
return self.raw_id[:12] | ||||
@LazyProperty | ||||
def parents(self): | ||||
""" | ||||
Returns list of parents changesets. | ||||
""" | ||||
return [self.repository.get_changeset(parent.rev()) | ||||
for parent in self._ctx.parents() if parent.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') | ||||
def _next(changeset, branch): | ||||
try: | ||||
next_ = changeset.revision + 1 | ||||
next_rev = changeset.repository.revisions[next_] | ||||
except IndexError: | ||||
raise ChangesetDoesNotExistError | ||||
cs = changeset.repository.get_changeset(next_rev) | ||||
if branch and branch != cs.branch: | ||||
return _next(cs, branch) | ||||
return cs | ||||
return _next(self, branch) | ||||
def prev(self, branch=None): | ||||
if branch and self.branch != branch: | ||||
raise VCSError('Branch option used on changeset not belonging ' | ||||
'to that branch') | ||||
def _prev(changeset, branch): | ||||
try: | ||||
prev_ = changeset.revision - 1 | ||||
if prev_ < 0: | ||||
raise IndexError | ||||
prev_rev = changeset.repository.revisions[prev_] | ||||
except IndexError: | ||||
raise ChangesetDoesNotExistError | ||||
cs = changeset.repository.get_changeset(prev_rev) | ||||
if branch and branch != cs.branch: | ||||
return _prev(cs, branch) | ||||
return cs | ||||
return _prev(self, branch) | ||||
def _fix_path(self, path): | ||||
""" | ||||
Paths are stored without trailing slash so we need to get rid off it if | ||||
needed. Also mercurial keeps filenodes as str so we need to decode | ||||
from unicode to str | ||||
""" | ||||
if path.endswith('/'): | ||||
path = path.rstrip('/') | ||||
return safe_str(path) | ||||
def _get_kind(self, path): | ||||
path = self._fix_path(path) | ||||
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 %r" | ||||
% (path)) | ||||
def _get_filectx(self, path): | ||||
path = self._fix_path(path) | ||||
if self._get_kind(path) != NodeKind.FILE: | ||||
raise ChangesetError("File does not exist for revision %r at " | ||||
" %r" % (self.revision, path)) | ||||
return self._ctx.filectx(path) | ||||
r2232 | def _extract_submodules(self): | |||
""" | ||||
returns a dictionary with submodule information from substate file | ||||
of hg repository | ||||
""" | ||||
return self._ctx.substate | ||||
r2007 | def get_file_mode(self, path): | |||
""" | ||||
Returns stat mode of the file at the given ``path``. | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
if 'x' in fctx.flags(): | ||||
return 0100755 | ||||
else: | ||||
return 0100644 | ||||
def get_file_content(self, path): | ||||
""" | ||||
Returns content of the file at given ``path``. | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
return fctx.data() | ||||
def get_file_size(self, path): | ||||
""" | ||||
Returns size of the file at given ``path``. | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
return fctx.size() | ||||
def get_file_changeset(self, path): | ||||
""" | ||||
Returns last commit of the file at the given ``path``. | ||||
""" | ||||
r2084 | node = self.get_node(path) | |||
return node.history[0] | ||||
r2007 | ||||
def get_file_history(self, path): | ||||
""" | ||||
Returns history of file as reversed list of ``Changeset`` objects for | ||||
which file at given ``path`` has been modified. | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
nodes = [fctx.filectx(x).node() for x in fctx.filelog()] | ||||
changesets = [self.repository.get_changeset(hex(node)) | ||||
for node in reversed(nodes)] | ||||
return changesets | ||||
def get_file_annotate(self, path): | ||||
""" | ||||
Returns a list of three element tuples with lineno,changeset and line | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
annotate = [] | ||||
for i, annotate_data in enumerate(fctx.annotate()): | ||||
ln_no = i + 1 | ||||
annotate.append((ln_no, self.repository\ | ||||
.get_changeset(hex(annotate_data[0].node())), | ||||
annotate_data[1],)) | ||||
return annotate | ||||
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.keys() | ||||
if kind not in allowed_kinds: | ||||
raise ImproperArchiveTypeError('Archive kind not supported use one' | ||||
'of %s', 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") | ||||
archival.archive(self.repository._repo, stream, self.raw_id, | ||||
kind, prefix=prefix, subrepos=subrepos) | ||||
r2278 | ||||
r2007 | if stream.closed and hasattr(stream, 'name'): | |||
stream = open(stream.name, 'rb') | ||||
elif hasattr(stream, 'mode') and 'r' not in stream.mode: | ||||
stream = open(stream.name, 'rb') | ||||
else: | ||||
stream.seek(0) | ||||
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 %r at " | ||||
" %r" % (self.revision, path)) | ||||
path = self._fix_path(path) | ||||
r2232 | ||||
r2007 | 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] | ||||
r2232 | ||||
als = self.repository.alias | ||||
for k, vals in self._extract_submodules().iteritems(): | ||||
#vals = url,rev,type | ||||
loc = vals[0] | ||||
cs = vals[1] | ||||
dirnodes.append(SubModuleNode(k, url=loc, changeset=cs, | ||||
alias=als)) | ||||
r2007 | nodes = dirnodes + filenodes | |||
# cache nodes | ||||
for node in nodes: | ||||
self.nodes[node.path] = node | ||||
nodes.sort() | ||||
r2232 | ||||
r2007 | 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. | ||||
""" | ||||
path = self._fix_path(path) | ||||
if not path in self.nodes: | ||||
if path in self._file_paths: | ||||
node = FileNode(path, changeset=self) | ||||
elif path in self._dir_paths or path in self._dir_paths: | ||||
if path == '': | ||||
node = RootNode(changeset=self) | ||||
else: | ||||
node = DirNode(path, changeset=self) | ||||
else: | ||||
raise NodeDoesNotExistError("There is no file nor directory " | ||||
"at the given path: %r at revision %r" | ||||
% (path, self.short_id)) | ||||
# cache node | ||||
self.nodes[path] = node | ||||
return self.nodes[path] | ||||
@LazyProperty | ||||
def affected_files(self): | ||||
""" | ||||
Get's a fast accessible file changes for given changeset | ||||
""" | ||||
return self._ctx.files() | ||||
@property | ||||
def added(self): | ||||
""" | ||||
Returns list of added ``FileNode`` objects. | ||||
""" | ||||
return AddedFileNodesGenerator([n for n in self.status[1]], self) | ||||
@property | ||||
def changed(self): | ||||
""" | ||||
Returns list of modified ``FileNode`` objects. | ||||
""" | ||||
return ChangedFileNodesGenerator([n for n in self.status[0]], self) | ||||
@property | ||||
def removed(self): | ||||
""" | ||||
Returns list of removed ``FileNode`` objects. | ||||
""" | ||||
return RemovedFileNodesGenerator([n for n in self.status[2]], self) | ||||