import re
from itertools import chain
from dulwich import objects
from subprocess import Popen, PIPE
from rhodecode.lib.vcs.conf import settings
from rhodecode.lib.vcs.exceptions import RepositoryError
from rhodecode.lib.vcs.exceptions import ChangesetError
from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
from rhodecode.lib.vcs.exceptions import VCSError
from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
from rhodecode.lib.vcs.backends.base import BaseChangeset
from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
RemovedFileNode, SubModuleNode
from rhodecode.lib.vcs.utils import safe_unicode
from rhodecode.lib.vcs.utils import date_fromtimestamp
from rhodecode.lib.vcs.utils.lazy import LazyProperty
class GitChangeset(BaseChangeset):
"""
Represents state of the repository at single revision.
def __init__(self, repository, revision):
self._stat_modes = {}
self.repository = repository
self.raw_id = revision
self.revision = repository.revisions.index(revision)
self.short_id = self.raw_id[:12]
self.id = self.raw_id
try:
commit = self.repository._repo.get_object(self.raw_id)
except KeyError:
raise RepositoryError("Cannot get object with id %s" % self.raw_id)
self._commit = commit
self._tree_id = commit.tree
self.message = safe_unicode(commit.message[:-1])
# Always strip last eol
except UnicodeDecodeError:
self.message = commit.message[:-1].decode(commit.encoding
or 'utf-8')
#self.branch = None
self.tags = []
#tree = self.repository.get_object(self._tree_id)
self.nodes = {}
self._paths = {}
@LazyProperty
def author(self):
return safe_unicode(self._commit.committer)
def date(self):
return date_fromtimestamp(self._commit.commit_time,
self._commit.commit_timezone)
def status(self):
@@ -284,97 +285,103 @@ class GitChangeset(BaseChangeset):
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 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")
if kind == 'zip':
frmt = 'zip'
else:
frmt = 'tar'
cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
self.raw_id)
if kind == 'tgz':
cmd += ' | gzip -9'
elif kind == 'tbz2':
cmd += ' | bzip2 -9'
if stream is None:
raise VCSError('You need to pass in a valid stream for filling'
' with archival data')
popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
cwd=self.repository.path)
buffer_size = 1024 * 8
chunk = popen.stdout.read(buffer_size)
while chunk:
stream.write(chunk)
# Make sure all descriptors would be read
popen.communicate()
def get_nodes(self, path):
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)
id = self._get_id_for_path(path)
tree = self.repository._repo[id]
dirnodes = []
filenodes = []
als = self.repository.alias
for name, stat, id in tree.iteritems():
if objects.S_ISGITLINK(stat):
dirnodes.append(SubModuleNode(name, url=None, changeset=id,
alias=als))
continue
obj = self.repository._repo.get_object(id)
if path != '':
obj_path = '/'.join((path, name))
obj_path = name
if obj_path not in self._stat_modes:
self._stat_modes[obj_path] = stat
if isinstance(obj, objects.Tree):
dirnodes.append(DirNode(obj_path, changeset=self))
elif isinstance(obj, objects.Blob):
filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
raise ChangesetError("Requested object should be Tree "
"or Blob, is %r" % type(obj))
nodes = dirnodes + filenodes
for node in nodes:
if not node.path in self.nodes:
self.nodes[node.path] = node
nodes.sort()
return nodes
def get_node(self, path):
if isinstance(path, unicode):
path = path.encode('utf-8')
if not path in self.nodes:
except ChangesetError:
raise NodeDoesNotExistError("Cannot find one of parents' "
"directories for a given path: %s" % path)
if path == '':
node = RootNode(changeset=self)
node = DirNode(path, changeset=self)
node._tree = obj
node = FileNode(path, changeset=self)
node._blob = obj
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]
import os
import posixpath
from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \
DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode
from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
RemovedFileNodesGenerator, RootNode, SubModuleNode
from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
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.
self._ctx = repository._repo[revision]
self.revision = self._ctx._rev
def tags(self):
return map(safe_unicode, self._ctx.tags())
def branch(self):
return safe_unicode(self._ctx.branch())
def message(self):
return safe_unicode(self._ctx.description())
return safe_unicode(self._ctx.user())
return date_fromtimestamp(*self._ctx.date())
Returns modified, added, removed, deleted files for current changeset
return self.repository._repo.status(self._ctx.p1().node(),
self._ctx.node())
@@ -114,96 +115,103 @@ class MercurialChangeset(BaseChangeset):
'to that branch')
def _prev(changeset, branch):
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):
if path in self._file_paths:
return NodeKind.FILE
elif path in self._dir_paths:
return NodeKind.DIR
raise ChangesetError("Node does not exist at the given path %r"
% (path))
def _get_filectx(self, path):
if self._get_kind(path) != NodeKind.FILE:
raise ChangesetError("File does not exist for revision %r at "
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)
if 'x' in fctx.flags():
return 0100755
return 0100644
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``.
node = self.get_node(path)
return node.history[0]
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.
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
annotate = []
@@ -226,107 +234,117 @@ class MercurialChangeset(BaseChangeset):
: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
archival.archive(self.repository._repo, stream, self.raw_id,
kind, prefix=prefix, subrepos=subrepos)
#stream.close()
if stream.closed and hasattr(stream, 'name'):
stream = open(stream.name, 'rb')
elif hasattr(stream, 'mode') and 'r' not in stream.mode:
stream.seek(0)
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.
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]
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,
# cache nodes
Returns ``Node`` object from the given ``path``. If there is no node at
the given ``path``, ``ChangesetError`` would be raised.
elif path in self._dir_paths or path in self._dir_paths:
"at the given path: %r at revision %r"
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)
def changed(self):
Returns list of modified ``FileNode`` objects.
return ChangedFileNodesGenerator([n for n in self.status[0]], self)
# -*- 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 stat
import mimetypes
from pygments import lexers
from rhodecode.lib.vcs.utils import safe_unicode, safe_str
from rhodecode.lib.vcs.exceptions import NodeError
from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
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:
class AddedFileNodesGenerator(NodeGeneratorBase):
Class holding Added files for current changeset
pass
class ChangedFileNodesGenerator(NodeGeneratorBase):
Class holding Changed files for current changeset
@@ -164,96 +166,103 @@ class Node(object):
# 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``
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``
return self.kind == NodeKind.SUBMODULE
return self.state is NodeState.ADDED
return self.state is NodeState.CHANGED
def not_changed(self):
return self.state is NodeState.NOT_CHANGED
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
def mode(self):
@@ -516,48 +525,76 @@ class DirNode(Node):
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
raise NodeError("Node does not exist at %s" % path)
def state(self):
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
return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
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
def __init__(self, name, url=None, changeset=None, alias=None):
self.path = name
self.kind = NodeKind.SUBMODULE
self.alias = alias
# changeset MUST be STR !! since it can point to non-valid SCM
self.changeset = str(changeset)
self.url = url or self._extract_submodule_url()
def _extract_submodule_url(self):
if self.alias == 'git':
return self.path
if self.alias == 'hg':
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[:12])
@@ -2673,96 +2673,104 @@ table.code-browser .browser-file {
}
.diffblock .changeset_header {
height: 16px;
.diffblock .changeset_file {
background: url("../images/icons/file.png") no-repeat scroll 3px;
text-align: left;
float: left;
padding: 2px 0px 2px 22px;
.diffblock .diff-menu-wrapper{
.diffblock .diff-menu{
position: absolute;
background: none repeat scroll 0 0 #FFFFFF;
border-color: #003367 #666666 #666666;
border-right: 1px solid #666666;
border-style: solid solid solid;
border-width: 1px;
box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
margin-top:5px;
margin-left:1px;
.diffblock .diff-actions {
padding: 2px 0px 0px 2px;
.diffblock .diff-menu ul li {
padding: 0px 0px 0px 0px !important;
.diffblock .diff-menu ul li a{
display: block;
padding: 3px 8px 3px 8px !important;
.diffblock .diff-menu ul li a:hover{
text-decoration: none;
background-color: #EEEEEE;
table.code-browser .browser-dir {
background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
padding-left: 20px;
table.code-browser .submodule-dir {
background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
.box .search {
clear: both;
overflow: hidden;
margin: 0;
padding: 0 20px 10px;
.box .search div.search_path {
background: none repeat scroll 0 0 #EEE;
border: 1px solid #CCC;
color: blue;
margin-bottom: 10px;
padding: 10px 0;
.box .search div.search_path div.link {
font-weight: 700;
margin-left: 25px;
.box .search div.search_path div.link a {
color: #003367;
cursor: pointer;
#path_unlock {
color: red;
font-size: 1.2em;
padding-left: 4px;
.info_box span {
margin-left: 3px;
margin-right: 3px;
.info_box .rev {
font-size: 1.6em;
font-weight: bold;
vertical-align: sub;
.info_box input#at_rev,.info_box input#size {
background: #FFF;
border-top: 1px solid #b3b3b3;
border-left: 1px solid #b3b3b3;
@@ -25,88 +25,92 @@
<div class="browser-search">
<div id="search_activate_id" class="search_activate">
<a class="ui-btn" id="filter_activate" href="#">${_('search file list')}</a>
</div>
% if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
<div id="add_node_id" class="add_node">
<a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}">${_('add new file')}</a>
% endif
<div>
<div id="node_filter_box_loading" style="display:none">${_('Loading file list...')}</div>
<div id="node_filter_box" style="display:none">
${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}/<input class="init" type="text" value="type to search..." name="filter" size="25" id="node_filter" autocomplete="off">
<div class="browser-body">
<table class="code-browser">
<thead>
<tr>
<th>${_('Name')}</th>
<th>${_('Size')}</th>
<th>${_('Mimetype')}</th>
<th>${_('Last Revision')}</th>
<th>${_('Last modified')}</th>
<th>${_('Last commiter')}</th>
</tr>
</thead>
<tbody id="tbody">
%if c.file.parent:
<tr class="parity0">
<td>
${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.file.parent.path),class_="browser-dir ypjax-link")}
</td>
<td></td>
%endif
%for cnt,node in enumerate(c.file):
<tr class="parity${cnt%2}">
${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
%if node.is_submodule():
${h.link_to(node.name,node.url or '#',class_="submodule-dir ypjax-link")}
%else:
${h.link_to(node.name, h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
%endif:
%if node.is_file():
${h.format_byte_size(node.size,binary=True)}
${node.mimetype}
<div class="tooltip" title="${node.last_changeset.message}">
<pre>${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}</pre>
<span class="tooltip" title="${node.last_changeset.date}">
${h.age(node.last_changeset.date)}</span>
<span title="${node.last_changeset.author}">
${h.person(node.last_changeset.author)}
</span>
%endfor
</tbody>
<tbody id="tbody_filtered" style="display:none">
</table>
Status change: