changeset.py
410 lines
| 13.0 KiB
| text/x-python
|
PythonLexer
|
r4187 | import os | ||
import posixpath | ||||
from kallithea.lib.vcs.backends.base import BaseChangeset | ||||
|
r7718 | from kallithea.lib.vcs.conf import settings | ||
from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError | ||||
|
r4187 | from kallithea.lib.vcs.nodes import ( | ||
|
r7718 | AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode) | ||
from kallithea.lib.vcs.utils import date_fromtimestamp, safe_str, safe_unicode | ||||
from kallithea.lib.vcs.utils.hgcompat import archival, hex, obsutil | ||||
|
r4187 | from kallithea.lib.vcs.utils.lazy import LazyProperty | ||
from kallithea.lib.vcs.utils.paths import get_dirs_for_path | ||||
|
r6789 | |||
|
r4187 | class MercurialChangeset(BaseChangeset): | ||
""" | ||||
|
r7903 | Represents state of the repository at a revision. | ||
|
r4187 | """ | ||
def __init__(self, repository, revision): | ||||
self.repository = repository | ||||
|
r4364 | assert isinstance(revision, basestring), repr(revision) | ||
|
r4187 | self.raw_id = revision | ||
self._ctx = repository._repo[revision] | ||||
self.revision = self._ctx._rev | ||||
self.nodes = {} | ||||
@LazyProperty | ||||
def tags(self): | ||||
|
r7892 | return [safe_unicode(tag) for tag in self._ctx.tags()] | ||
|
r4187 | |||
@LazyProperty | ||||
def branch(self): | ||||
|
r6789 | return safe_unicode(self._ctx.branch()) | ||
|
r4187 | |||
@LazyProperty | ||||
|
r7067 | def branches(self): | ||
return [safe_unicode(self._ctx.branch())] | ||||
@LazyProperty | ||||
|
r4348 | def closesbranch(self): | ||
|
r6074 | return self._ctx.closesbranch() | ||
|
r4348 | |||
@LazyProperty | ||||
|
r5347 | def obsolete(self): | ||
|
r6074 | return self._ctx.obsolete() | ||
@LazyProperty | ||||
def bumped(self): | ||||
|
r7913 | return self._ctx.phasedivergent() | ||
|
r6074 | |||
@LazyProperty | ||||
def divergent(self): | ||||
|
r7913 | return self._ctx.contentdivergent() | ||
|
r6074 | |||
@LazyProperty | ||||
def extinct(self): | ||||
return self._ctx.extinct() | ||||
@LazyProperty | ||||
def unstable(self): | ||||
|
r7913 | return self._ctx.orphan() | ||
|
r6074 | |||
@LazyProperty | ||||
def phase(self): | ||||
if(self._ctx.phase() == 1): | ||||
return 'Draft' | ||||
elif(self._ctx.phase() == 2): | ||||
return 'Secret' | ||||
else: | ||||
return '' | ||||
|
r5347 | |||
@LazyProperty | ||||
|
r5349 | def successors(self): | ||
|
r7615 | successors = obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True) | ||
|
r5349 | if successors: | ||
# flatten the list here handles both divergent (len > 1) | ||||
# and the usual case (len = 1) | ||||
successors = [hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()] | ||||
return successors | ||||
@LazyProperty | ||||
|
r7445 | def predecessors(self): | ||
|
r7615 | return [hex(n)[:12] for n in obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())] | ||
|
r5351 | |||
@LazyProperty | ||||
|
r4187 | def bookmarks(self): | ||
|
r7892 | return [safe_unicode(bookmark) for bookmark in self._ctx.bookmarks()] | ||
|
r4187 | |||
@LazyProperty | ||||
def message(self): | ||||
return safe_unicode(self._ctx.description()) | ||||
@LazyProperty | ||||
def committer(self): | ||||
return safe_unicode(self.author) | ||||
@LazyProperty | ||||
def author(self): | ||||
return safe_unicode(self._ctx.user()) | ||||
@LazyProperty | ||||
def date(self): | ||||
return date_fromtimestamp(*self._ctx.date()) | ||||
@LazyProperty | ||||
def _timestamp(self): | ||||
return self._ctx.date()[0] | ||||
@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] | ||||
@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') | ||||
|
r4400 | cs = self | ||
while True: | ||||
|
r4187 | try: | ||
|
r6702 | next_ = cs.repository.revisions.index(cs.raw_id) + 1 | ||
|
r4400 | next_rev = cs.repository.revisions[next_] | ||
|
r4187 | except IndexError: | ||
raise ChangesetDoesNotExistError | ||||
|
r4400 | cs = cs.repository.get_changeset(next_rev) | ||
|
r4187 | |||
|
r4400 | if not branch or branch == cs.branch: | ||
return cs | ||||
|
r4187 | |||
def prev(self, branch=None): | ||||
if branch and self.branch != branch: | ||||
raise VCSError('Branch option used on changeset not belonging ' | ||||
'to that branch') | ||||
|
r4400 | cs = self | ||
while True: | ||||
|
r4187 | try: | ||
|
r6702 | prev_ = cs.repository.revisions.index(cs.raw_id) - 1 | ||
|
r4187 | if prev_ < 0: | ||
raise IndexError | ||||
|
r4400 | prev_rev = cs.repository.revisions[prev_] | ||
|
r4187 | except IndexError: | ||
raise ChangesetDoesNotExistError | ||||
|
r4400 | cs = cs.repository.get_changeset(prev_rev) | ||
|
r4187 | |||
|
r4400 | if not branch or branch == cs.branch: | ||
return cs | ||||
|
r4187 | |||
|
r7266 | def diff(self): | ||
|
r7903 | # Only used to feed diffstat | ||
|
r7958 | return b''.join(self._ctx.diff()) | ||
|
r4187 | |||
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 '%s'" | ||||
% (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 %s at " | ||||
" '%s'" % (self.raw_id, path)) | ||||
return self._ctx.filectx(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) | ||||
|
r7958 | if b'x' in fctx.flags(): | ||
|
r7890 | return 0o100755 | ||
|
r4187 | else: | ||
|
r7890 | return 0o100644 | ||
|
r4187 | |||
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``. | ||||
""" | ||||
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. | ||||
""" | ||||
fctx = self._get_filectx(path) | ||||
hist = [] | ||||
cnt = 0 | ||||
for cs in reversed([x for x in fctx.filelog()]): | ||||
cnt += 1 | ||||
hist.append(hex(fctx.filectx(cs).node())) | ||||
|
r5564 | if limit is not None and cnt == limit: | ||
|
r4187 | 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 | ||||
""" | ||||
|
r7198 | annotations = self._get_filectx(path).annotate() | ||
|
r7913 | annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations] | ||
|
r7197 | for i, (fctx, l) in enumerate(annotation_lines): | ||
|
r7196 | sha = fctx.hex() | ||
yield (i + 1, sha, lambda sha=sha, l=l: self.repository.get_changeset(sha), l) | ||||
|
r4187 | |||
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 | ||||
""" | ||||
|
r7906 | allowed_kinds = settings.ARCHIVE_SPECS | ||
|
r4187 | if kind not in allowed_kinds: | ||
raise ImproperArchiveTypeError('Archive kind not supported use one' | ||||
|
r7906 | 'of %s' % ' '.join(allowed_kinds)) | ||
|
r4187 | |||
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) | ||||
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)) | ||||
path = self._fix_path(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().iteritems(): | ||||
#vals = url,rev,type | ||||
loc = vals[0] | ||||
cs = vals[1] | ||||
dirnodes.append(SubModuleNode(k, url=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. | ||||
""" | ||||
path = self._fix_path(path) | ||||
|
r6791 | if path not in self.nodes: | ||
|
r4187 | 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: '%s' at revision %s" | ||||
% (path, self.short_id)) | ||||
# cache node | ||||
self.nodes[path] = node | ||||
return self.nodes[path] | ||||
@LazyProperty | ||||
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([n for n in self.status[1]], self) | ||||
@property | ||||
def changed(self): | ||||
""" | ||||
Returns list of modified ``FileNode`` objects. | ||||
""" | ||||
|
r6789 | return ChangedFileNodesGenerator([n for n in self.status[0]], self) | ||
|
r4187 | |||
@property | ||||
def removed(self): | ||||
""" | ||||
Returns list of removed ``FileNode`` objects. | ||||
""" | ||||
return RemovedFileNodesGenerator([n for n in self.status[2]], self) | ||||
|
r4375 | |||
@LazyProperty | ||||
def extra(self): | ||||
return self._ctx.extra() | ||||