Commits

Marcin Kuzminski committed 324ac36

Added VCS into rhodecode core for faster and easier deployments of new versions

  • Participants
  • Parent commits 34d009e

Comments (0)

Files changed (59)

 ^test\.db$
 ^RhodeCode\.egg-info$
 ^rc\.ini$
+^fabfile.py

File requires.txt

 babel
 python-dateutil>=1.5.0,<2.0.0
 dulwich>=0.8.0,<0.9.0
-vcs>=0.2.3.dev
 webob==1.0.8
-markdown==2.0.3
+markdown==2.1.1
 docutils==0.8.1
 py-bcrypt
 mercurial>=2.1,<2.2

File rhodecode/__init__.py

     "babel",
     "python-dateutil>=1.5.0,<2.0.0",
     "dulwich>=0.8.0,<0.9.0",
-    "vcs>=0.2.3.dev",
     "webob==1.0.8",
-    "markdown==2.0.3",
+    "markdown==2.1.1",
     "docutils==0.8.1",
 ]
 

File rhodecode/controllers/changelog.py

 from rhodecode.lib.helpers import RepoPage
 from rhodecode.lib.compat import json
 
-from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
+from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
 from rhodecode.model.db import Repository
 
 log = logging.getLogger(__name__)

File rhodecode/controllers/changeset.py

 from pylons.controllers.util import redirect
 from pylons.decorators import jsonify
 
-from vcs.exceptions import RepositoryError, ChangesetError, \
+from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
     ChangesetDoesNotExistError
-from vcs.nodes import FileNode
+from rhodecode.lib.vcs.nodes import FileNode
 
 import rhodecode.lib.helpers as h
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator

File rhodecode/controllers/files.py

 from pylons.controllers.util import redirect
 from pylons.decorators import jsonify
 
-from vcs.conf import settings
-from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
+from rhodecode.lib.vcs.conf import settings
+from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
     EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
     NodeAlreadyExistsError
-from vcs.nodes import FileNode
+from rhodecode.lib.vcs.nodes import FileNode
 
 from rhodecode.lib.compat import OrderedDict
 from rhodecode.lib import convert_line_endings, detect_mode, safe_str

File rhodecode/controllers/summary.py

 from itertools import product
 from urlparse import urlparse
 
-from vcs.exceptions import ChangesetError, EmptyRepositoryError, \
+from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
     NodeDoesNotExistError
 
 from pylons import tmpl_context as c, request, url, config

File rhodecode/lib/__init__.py

 
 import os
 import re
-from vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
 
 def __get_lem():
 # extension together with weights to search lower is first
 RST_EXTS = [
     ('', 0), ('.rst', 1), ('.rest', 1),
-    ('.RST', 2) , ('.REST', 2),
+    ('.RST', 2), ('.REST', 2),
     ('.txt', 3), ('.TXT', 3)
 ]
 
             line = replace(line, '\r\n', '\r')
             line = replace(line, '\n', '\r')
     elif mode == 2:
-            import re
             line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
     return line
 
     from datetime import datetime
     from webhelpers.date import time_ago_in_words
 
-    _ = lambda s:s
+    _ = lambda s: s
 
     if not curdate:
         return ''
     pos = 1
     for scale in agescales:
         if scale[1] <= age_seconds:
-            if pos == 6:pos = 5
+            if pos == 6:
+                pos = 5
             return '%s %s' % (time_ago_in_words(curdate,
                                                 agescales[pos][0]), _('ago'))
         pos += 1
     :param repo:
     :param rev:
     """
-    from vcs.backends.base import BaseRepository
-    from vcs.exceptions import RepositoryError
+    from rhodecode.lib.vcs.backends.base import BaseRepository
+    from rhodecode.lib.vcs.exceptions import RepositoryError
     if not isinstance(repo, BaseRepository):
         raise Exception('You must pass an Repository '
                         'object as first argument got %s', type(repo))
     """
 
     try:
-        from vcs import get_repo
-        from vcs.utils.helpers import get_scm
+        from rhodecode.lib.vcs import get_repo
+        from rhodecode.lib.vcs.utils.helpers import get_scm
         repopath = os.path.join(os.path.dirname(__file__), '..', '..')
         scm = get_scm(repopath)[0]
         repo = get_repo(path=repopath, alias=scm)

File rhodecode/lib/annotate.py

     :license: GPLv3, see COPYING for more details.
 """
 
-from vcs.exceptions import VCSError
-from vcs.nodes import FileNode
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.nodes import FileNode
 from pygments.formatters import HtmlFormatter
 from pygments import highlight
 

File rhodecode/lib/celerylib/__init__.py

 from hashlib import md5
 from decorator import decorator
 
-from vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
 from rhodecode import CELERY_ON
 from rhodecode.lib import str2bool, safe_str
 from rhodecode.lib.pidlock import DaemonLock, LockHeld

File rhodecode/lib/celerylib/tasks.py

 from pylons import config, url
 from pylons.i18n.translation import _
 
-from vcs import get_backend
+from rhodecode.lib.vcs import get_backend
 
 from rhodecode import CELERY_ON
 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str

File rhodecode/lib/dbmigrate/schema/db_1_2_0.py

 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
 from beaker.cache import cache_region, region_invalidate
 
-from vcs import get_backend
-from vcs.utils.helpers import get_scm
-from vcs.exceptions import VCSError
-from vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs import get_backend
+from rhodecode.lib.vcs.utils.helpers import get_scm
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \
     generate_api_key, safe_unicode

File rhodecode/lib/diffs.py

 
 from pylons.i18n.translation import _
 
-from vcs.exceptions import VCSError
-from vcs.nodes import FileNode
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.nodes import FileNode
 
 from rhodecode.lib.utils import EmptyChangeset
 

File rhodecode/lib/helpers.py

 #==============================================================================
 # SCM FILTERS available via h.
 #==============================================================================
-from vcs.utils import author_name, author_email
+from rhodecode.lib.vcs.utils import author_name, author_email
 from rhodecode.lib import credentials_filter, age as _age
 from rhodecode.model.db import User
 

File rhodecode/lib/indexers/daemon.py

 from rhodecode.lib import safe_unicode
 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
 
-from vcs.exceptions import ChangesetError, RepositoryError, \
+from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \
     NodeDoesNotExistError
 
 from whoosh.index import create_in, open_dir

File rhodecode/lib/middleware/simplegit.py

 
 from dulwich import server as dulserver
 
+
 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
 
     def handle(self):

File rhodecode/lib/middleware/simplehg.py

         baseui = make_ui('db')
         self.__inject_extras(repo_path, baseui, extras)
 
-
         # quick check if that dir exists...
         if is_valid_repo(repo_name, self.basepath) is False:
             return HTTPNotFound()(environ, start_response)
                 else:
                     return 'pull'
 
-
     def __inject_extras(self, repo_path, baseui, extras={}):
         """
         Injects some extra params into baseui instance

File rhodecode/lib/utils.py

 
 from webhelpers.text import collapse, remove_formatting, strip_tags
 
-from vcs import get_backend
-from vcs.backends.base import BaseChangeset
-from vcs.utils.lazy import LazyProperty
-from vcs.utils.helpers import get_scm
-from vcs.exceptions import VCSError
+from rhodecode.lib.vcs import get_backend
+from rhodecode.lib.vcs.backends.base import BaseChangeset
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs.utils.helpers import get_scm
+from rhodecode.lib.vcs.exceptions import VCSError
 
 from rhodecode.lib.caching_query import FromCache
 

File rhodecode/lib/vcs/__init__.py

+# -*- coding: utf-8 -*-
+"""
+    vcs
+    ~~~
+
+    Various version Control System (vcs) management abstraction layer for
+    Python.
+
+    :created_on: Apr 8, 2010
+    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
+"""
+
+VERSION = (0, 2, 3, 'dev')
+
+__version__ = '.'.join((str(each) for each in VERSION[:4]))
+
+__all__ = [
+    'get_version', 'get_repo', 'get_backend',
+    'VCSError', 'RepositoryError', 'ChangesetError']
+
+import sys
+from rhodecode.lib.vcs.backends import get_repo, get_backend
+from rhodecode.lib.vcs.exceptions import VCSError, RepositoryError, ChangesetError
+
+
+def get_version():
+    """
+    Returns shorter version (digit parts only) as string.
+    """
+    return '.'.join((str(each) for each in VERSION[:3]))
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv
+    from rhodecode.lib.vcs.cli import ExecutionManager
+    manager = ExecutionManager(argv)
+    manager.execute()
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))

File rhodecode/lib/vcs/backends/__init__.py

+# -*- coding: utf-8 -*-
+"""
+    vcs.backends
+    ~~~~~~~~~~~~
+
+    Main package for scm backends
+
+    :created_on: Apr 8, 2010
+    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
+"""
+import os
+from pprint import pformat
+from rhodecode.lib.vcs.conf import settings
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.utils.helpers import get_scm
+from rhodecode.lib.vcs.utils.paths import abspath
+from rhodecode.lib.vcs.utils.imports import import_class
+
+
+def get_repo(path=None, alias=None, create=False):
+    """
+    Returns ``Repository`` object of type linked with given ``alias`` at
+    the specified ``path``. If ``alias`` is not given it will try to guess it
+    using get_scm method
+    """
+    if create:
+        if not (path or alias):
+            raise TypeError("If create is specified, we need path and scm type")
+        return get_backend(alias)(path, create=True)
+    if path is None:
+        path = abspath(os.path.curdir)
+    try:
+        scm, path = get_scm(path, search_recursively=True)
+        path = abspath(path)
+        alias = scm
+    except VCSError:
+        raise VCSError("No scm found at %s" % path)
+    if alias is None:
+        alias = get_scm(path)[0]
+
+    backend = get_backend(alias)
+    repo = backend(path, create=create)
+    return repo
+
+
+def get_backend(alias):
+    """
+    Returns ``Repository`` class identified by the given alias or raises
+    VCSError if alias is not recognized or backend class cannot be imported.
+    """
+    if alias not in settings.BACKENDS:
+        raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n"
+            "%s" % (alias, pformat(settings.BACKENDS.keys())))
+    backend_path = settings.BACKENDS[alias]
+    klass = import_class(backend_path)
+    return klass
+
+
+def get_supported_backends():
+    """
+    Returns list of aliases of supported backends.
+    """
+    return settings.BACKENDS.keys()

File rhodecode/lib/vcs/backends/base.py

+# -*- coding: utf-8 -*-
+"""
+    vcs.backends.base
+    ~~~~~~~~~~~~~~~~~
+
+    Base for all available scm backends
+
+    :created_on: Apr 8, 2010
+    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
+"""
+
+
+from itertools import chain
+from rhodecode.lib.vcs.utils import author_name, author_email
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
+from rhodecode.lib.vcs.conf import settings
+
+from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
+    NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
+    NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
+    RepositoryError
+
+
+class BaseRepository(object):
+    """
+    Base Repository for final backends
+
+    **Attributes**
+
+        ``DEFAULT_BRANCH_NAME``
+            name of default branch (i.e. "trunk" for svn, "master" for git etc.
+
+        ``scm``
+            alias of scm, i.e. *git* or *hg*
+
+        ``repo``
+            object from external api
+
+        ``revisions``
+            list of all available revisions' ids, in ascending order
+
+        ``changesets``
+            storage dict caching returned changesets
+
+        ``path``
+            absolute path to the repository
+
+        ``branches``
+            branches as list of changesets
+
+        ``tags``
+            tags as list of changesets
+    """
+    scm = None
+    DEFAULT_BRANCH_NAME = None
+    EMPTY_CHANGESET = '0' * 40
+
+    def __init__(self, repo_path, create=False, **kwargs):
+        """
+        Initializes repository. Raises RepositoryError if repository could
+        not be find at the given ``repo_path`` or directory at ``repo_path``
+        exists and ``create`` is set to True.
+
+        :param repo_path: local path of the repository
+        :param create=False: if set to True, would try to craete repository.
+        :param src_url=None: if set, should be proper url from which repository
+          would be cloned; requires ``create`` parameter to be set to True -
+          raises RepositoryError if src_url is set and create evaluates to
+          False
+        """
+        raise NotImplementedError
+
+    def __str__(self):
+        return '<%s at %s>' % (self.__class__.__name__, self.path)
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __len__(self):
+        return self.count()
+
+    @LazyProperty
+    def alias(self):
+        for k, v in settings.BACKENDS.items():
+            if v.split('.')[-1] == str(self.__class__.__name__):
+                return k
+
+    @LazyProperty
+    def name(self):
+        raise NotImplementedError
+
+    @LazyProperty
+    def owner(self):
+        raise NotImplementedError
+
+    @LazyProperty
+    def description(self):
+        raise NotImplementedError
+
+    @LazyProperty
+    def size(self):
+        """
+        Returns combined size in bytes for all repository files
+        """
+
+        size = 0
+        try:
+            tip = self.get_changeset()
+            for topnode, dirs, files in tip.walk('/'):
+                for f in files:
+                    size += tip.get_file_size(f.path)
+                for dir in dirs:
+                    for f in files:
+                        size += tip.get_file_size(f.path)
+
+        except RepositoryError, e:
+            pass
+        return size
+
+    def is_valid(self):
+        """
+        Validates repository.
+        """
+        raise NotImplementedError
+
+    def get_last_change(self):
+        self.get_changesets()
+
+    #==========================================================================
+    # CHANGESETS
+    #==========================================================================
+
+    def get_changeset(self, revision=None):
+        """
+        Returns instance of ``Changeset`` class. If ``revision`` is None, most
+        recent changeset is returned.
+
+        :raises ``EmptyRepositoryError``: if there are no revisions
+        """
+        raise NotImplementedError
+
+    def __iter__(self):
+        """
+        Allows Repository objects to be iterated.
+
+        *Requires* implementation of ``__getitem__`` method.
+        """
+        for revision in self.revisions:
+            yield self.get_changeset(revision)
+
+    def get_changesets(self, start=None, end=None, start_date=None,
+                       end_date=None, branch_name=None, reverse=False):
+        """
+        Returns iterator of ``MercurialChangeset`` objects from start to end
+        not inclusive This should behave just like a list, ie. end is not
+        inclusive
+
+        :param start: None or str
+        :param end: None or str
+        :param start_date:
+        :param end_date:
+        :param branch_name:
+        :param reversed:
+        """
+        raise NotImplementedError
+
+    def __getslice__(self, i, j):
+        """
+        Returns a iterator of sliced repository
+        """
+        for rev in self.revisions[i:j]:
+            yield self.get_changeset(rev)
+
+    def __getitem__(self, key):
+        return self.get_changeset(key)
+
+    def count(self):
+        return len(self.revisions)
+
+    def tag(self, name, user, revision=None, message=None, date=None, **opts):
+        """
+        Creates and returns a tag for the given ``revision``.
+
+        :param name: name for new tag
+        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
+        :param revision: changeset id for which new tag would be created
+        :param message: message of the tag's commit
+        :param date: date of tag's commit
+
+        :raises TagAlreadyExistError: if tag with same name already exists
+        """
+        raise NotImplementedError
+
+    def remove_tag(self, name, user, message=None, date=None):
+        """
+        Removes tag with the given ``name``.
+
+        :param name: name of the tag to be removed
+        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
+        :param message: message of the tag's removal commit
+        :param date: date of tag's removal commit
+
+        :raises TagDoesNotExistError: if tag with given name does not exists
+        """
+        raise NotImplementedError
+
+    def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
+            context=3):
+        """
+        Returns (git like) *diff*, as plain text. Shows changes introduced by
+        ``rev2`` since ``rev1``.
+
+        :param rev1: Entry point from which diff is shown. Can be
+          ``self.EMPTY_CHANGESET`` - in this case, patch showing all
+          the changes since empty state of the repository until ``rev2``
+        :param rev2: Until which revision changes should be shown.
+        :param ignore_whitespace: If set to ``True``, would not show whitespace
+          changes. Defaults to ``False``.
+        :param context: How many lines before/after changed lines should be
+          shown. Defaults to ``3``.
+        """
+        raise NotImplementedError
+
+    # ========== #
+    # COMMIT API #
+    # ========== #
+
+    @LazyProperty
+    def in_memory_changeset(self):
+        """
+        Returns ``InMemoryChangeset`` object for this repository.
+        """
+        raise NotImplementedError
+
+    def add(self, filenode, **kwargs):
+        """
+        Commit api function that will add given ``FileNode`` into this
+        repository.
+
+        :raises ``NodeAlreadyExistsError``: if there is a file with same path
+          already in repository
+        :raises ``NodeAlreadyAddedError``: if given node is already marked as
+          *added*
+        """
+        raise NotImplementedError
+
+    def remove(self, filenode, **kwargs):
+        """
+        Commit api function that will remove given ``FileNode`` into this
+        repository.
+
+        :raises ``EmptyRepositoryError``: if there are no changesets yet
+        :raises ``NodeDoesNotExistError``: if there is no file with given path
+        """
+        raise NotImplementedError
+
+    def commit(self, message, **kwargs):
+        """
+        Persists current changes made on this repository and returns newly
+        created changeset.
+
+        :raises ``NothingChangedError``: if no changes has been made
+        """
+        raise NotImplementedError
+
+    def get_state(self):
+        """
+        Returns dictionary with ``added``, ``changed`` and ``removed`` lists
+        containing ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    def get_config_value(self, section, name, config_file=None):
+        """
+        Returns configuration value for a given [``section``] and ``name``.
+
+        :param section: Section we want to retrieve value from
+        :param name: Name of configuration we want to retrieve
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    def get_user_name(self, config_file=None):
+        """
+        Returns user's name from global configuration file.
+
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    def get_user_email(self, config_file=None):
+        """
+        Returns user's email from global configuration file.
+
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    # =========== #
+    # WORKDIR API #
+    # =========== #
+
+    @LazyProperty
+    def workdir(self):
+        """
+        Returns ``Workdir`` instance for this repository.
+        """
+        raise NotImplementedError
+
+
+class BaseChangeset(object):
+    """
+    Each backend should implement it's changeset representation.
+
+    **Attributes**
+
+        ``repository``
+            repository object within which changeset exists
+
+        ``id``
+            may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
+
+        ``raw_id``
+            raw changeset representation (i.e. full 40 length sha for git
+            backend)
+
+        ``short_id``
+            shortened (if apply) version of ``raw_id``; it would be simple
+            shortcut for ``raw_id[:12]`` for git/mercurial backends or same
+            as ``raw_id`` for subversion
+
+        ``revision``
+            revision number as integer
+
+        ``files``
+            list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
+
+        ``dirs``
+            list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
+
+        ``nodes``
+            combined list of ``Node`` objects
+
+        ``author``
+            author of the changeset, as unicode
+
+        ``message``
+            message of the changeset, as unicode
+
+        ``parents``
+            list of parent changesets
+
+        ``last``
+            ``True`` if this is last changeset in repository, ``False``
+            otherwise; trying to access this attribute while there is no
+            changesets would raise ``EmptyRepositoryError``
+    """
+    def __str__(self):
+        return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
+            self.short_id)
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __unicode__(self):
+        return u'%s:%s' % (self.revision, self.short_id)
+
+    def __eq__(self, other):
+        return self.raw_id == other.raw_id
+
+    @LazyProperty
+    def last(self):
+        if self.repository is None:
+            raise ChangesetError("Cannot check if it's most recent revision")
+        return self.raw_id == self.repository.revisions[-1]
+
+    @LazyProperty
+    def parents(self):
+        """
+        Returns list of parents changesets.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def id(self):
+        """
+        Returns string identifying this changeset.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def raw_id(self):
+        """
+        Returns raw string identifying this changeset.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def short_id(self):
+        """
+        Returns shortened version of ``raw_id`` attribute, as string,
+        identifying this changeset, useful for web representation.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def revision(self):
+        """
+        Returns integer identifying this changeset.
+
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def author(self):
+        """
+        Returns Author for given commit
+        """
+
+        raise NotImplementedError
+
+    @LazyProperty
+    def author_name(self):
+        """
+        Returns Author name for given commit
+        """
+
+        return author_name(self.author)
+
+    @LazyProperty
+    def author_email(self):
+        """
+        Returns Author email address for given commit
+        """
+
+        return author_email(self.author)
+
+    def get_file_mode(self, path):
+        """
+        Returns stat mode of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_content(self, path):
+        """
+        Returns content of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_size(self, path):
+        """
+        Returns size of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_changeset(self, path):
+        """
+        Returns last commit of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    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.
+        """
+        raise NotImplementedError
+
+    def get_nodes(self, path):
+        """
+        Returns combined ``DirNode`` and ``FileNode`` objects list representing
+        state of changeset at the given ``path``.
+
+        :raises ``ChangesetError``: if node at the given ``path`` is not
+          instance of ``DirNode``
+        """
+        raise NotImplementedError
+
+    def get_node(self, path):
+        """
+        Returns ``Node`` object from the given ``path``.
+
+        :raises ``NodeDoesNotExistError``: if there is no node at the given
+          ``path``
+        """
+        raise NotImplementedError
+
+    def fill_archive(self, stream=None, kind='tgz', prefix=None):
+        """
+        Fills up given stream.
+
+        :param stream: file like object.
+        :param kind: one of following: ``zip``, ``tar``, ``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>
+        """
+
+        raise NotImplementedError
+
+    def get_chunked_archive(self, **kwargs):
+        """
+        Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
+
+        :param chunk_size: extra parameter which controls size of returned
+            chunks. Default:8k.
+        """
+
+        chunk_size = kwargs.pop('chunk_size', 8192)
+        stream = kwargs.get('stream')
+        self.fill_archive(**kwargs)
+        while True:
+            data = stream.read(chunk_size)
+            if not data:
+                break
+            yield data
+
+    @LazyProperty
+    def root(self):
+        """
+        Returns ``RootNode`` object for this changeset.
+        """
+        return self.get_node('')
+
+    def next(self, branch=None):
+        """
+        Returns next changeset from current, if branch is gives it will return
+        next changeset belonging to this branch
+
+        :param branch: show changesets within the given named branch
+        """
+        raise NotImplementedError
+
+    def prev(self, branch=None):
+        """
+        Returns previous changeset from current, if branch is gives it will
+        return previous changeset belonging to this branch
+
+        :param branch: show changesets within the given named branch
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def added(self):
+        """
+        Returns list of added ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def changed(self):
+        """
+        Returns list of modified ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def removed(self):
+        """
+        Returns list of removed ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def size(self):
+        """
+        Returns total number of bytes from contents of all filenodes.
+        """
+        return sum((node.size for node in self.get_filenodes_generator()))
+
+    def walk(self, topurl=''):
+        """
+        Similar to os.walk method. Insted of filesystem it walks through
+        changeset starting at given ``topurl``.  Returns generator of tuples
+        (topnode, dirnodes, filenodes).
+        """
+        topnode = self.get_node(topurl)
+        yield (topnode, topnode.dirs, topnode.files)
+        for dirnode in topnode.dirs:
+            for tup in self.walk(dirnode.path):
+                yield tup
+
+    def get_filenodes_generator(self):
+        """
+        Returns generator that yields *all* file nodes.
+        """
+        for topnode, dirs, files in self.walk():
+            for node in files:
+                yield node
+
+    def as_dict(self):
+        """
+        Returns dictionary with changeset's attributes and their values.
+        """
+        data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
+            'revision', 'date', 'message'])
+        data['author'] = {'name': self.author_name, 'email': self.author_email}
+        data['added'] = [node.path for node in self.added]
+        data['changed'] = [node.path for node in self.changed]
+        data['removed'] = [node.path for node in self.removed]
+        return data
+
+
+class BaseWorkdir(object):
+    """
+    Working directory representation of single repository.
+
+    :attribute: repository: repository object of working directory
+    """
+
+    def __init__(self, repository):
+        self.repository = repository
+
+    def get_branch(self):
+        """
+        Returns name of current branch.
+        """
+        raise NotImplementedError
+
+    def get_changeset(self):
+        """
+        Returns current changeset.
+        """
+        raise NotImplementedError
+
+    def get_added(self):
+        """
+        Returns list of ``FileNode`` objects marked as *new* in working
+        directory.
+        """
+        raise NotImplementedError
+
+    def get_changed(self):
+        """
+        Returns list of ``FileNode`` objects *changed* in working directory.
+        """
+        raise NotImplementedError
+
+    def get_removed(self):
+        """
+        Returns list of ``RemovedFileNode`` objects marked as *removed* in
+        working directory.
+        """
+        raise NotImplementedError
+
+    def get_untracked(self):
+        """
+        Returns list of ``FileNode`` objects which are present within working
+        directory however are not tracked by repository.
+        """
+        raise NotImplementedError
+
+    def get_status(self):
+        """
+        Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
+        lists.
+        """
+        raise NotImplementedError
+
+    def commit(self, message, **kwargs):
+        """
+        Commits local (from working directory) changes and returns newly
+        created
+        ``Changeset``. Updates repository's ``revisions`` list.
+
+        :raises ``CommitError``: if any error occurs while committing
+        """
+        raise NotImplementedError
+
+    def update(self, revision=None):
+        """
+        Fetches content of the given revision and populates it within working
+        directory.
+        """
+        raise NotImplementedError
+
+    def checkout_branch(self, branch=None):
+        """
+        Checks out ``branch`` or the backend's default branch.
+
+        Raises ``BranchDoesNotExistError`` if the branch does not exist.
+        """
+        raise NotImplementedError
+
+
+class BaseInMemoryChangeset(object):
+    """
+    Represents differences between repository's state (most recent head) and
+    changes made *in place*.
+
+    **Attributes**
+
+        ``repository``
+            repository object for this in-memory-changeset
+
+        ``added``
+            list of ``FileNode`` objects marked as *added*
+
+        ``changed``
+            list of ``FileNode`` objects marked as *changed*
+
+        ``removed``
+            list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
+            *removed*
+
+        ``parents``
+            list of ``Changeset`` representing parents of in-memory changeset.
+            Should always be 2-element sequence.
+
+    """
+
+    def __init__(self, repository):
+        self.repository = repository
+        self.added = []
+        self.changed = []
+        self.removed = []
+        self.parents = []
+
+    def add(self, *filenodes):
+        """
+        Marks given ``FileNode`` objects as *to be committed*.
+
+        :raises ``NodeAlreadyExistsError``: if node with same path exists at
+          latest changeset
+        :raises ``NodeAlreadyAddedError``: if node with same path is already
+          marked as *added*
+        """
+        # Check if not already marked as *added* first
+        for node in filenodes:
+            if node.path in (n.path for n in self.added):
+                raise NodeAlreadyAddedError("Such FileNode %s is already "
+                    "marked for addition" % node.path)
+        for node in filenodes:
+            self.added.append(node)
+
+    def change(self, *filenodes):
+        """
+        Marks given ``FileNode`` objects to be *changed* in next commit.
+
+        :raises ``EmptyRepositoryError``: if there are no changesets yet
+        :raises ``NodeAlreadyExistsError``: if node with same path is already
+          marked to be *changed*
+        :raises ``NodeAlreadyRemovedError``: if node with same path is already
+          marked to be *removed*
+        :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
+          changeset
+        :raises ``NodeNotChangedError``: if node hasn't really be changed
+        """
+        for node in filenodes:
+            if node.path in (n.path for n in self.removed):
+                raise NodeAlreadyRemovedError("Node at %s is already marked "
+                    "as removed" % node.path)
+        try:
+            self.repository.get_changeset()
+        except EmptyRepositoryError:
+            raise EmptyRepositoryError("Nothing to change - try to *add* new "
+                "nodes rather than changing them")
+        for node in filenodes:
+            if node.path in (n.path for n in self.changed):
+                raise NodeAlreadyChangedError("Node at '%s' is already "
+                    "marked as changed" % node.path)
+            self.changed.append(node)
+
+    def remove(self, *filenodes):
+        """
+        Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
+        *removed* in next commit.
+
+        :raises ``NodeAlreadyRemovedError``: if node has been already marked to
+          be *removed*
+        :raises ``NodeAlreadyChangedError``: if node has been already marked to
+          be *changed*
+        """
+        for node in filenodes:
+            if node.path in (n.path for n in self.removed):
+                raise NodeAlreadyRemovedError("Node is already marked to "
+                    "for removal at %s" % node.path)
+            if node.path in (n.path for n in self.changed):
+                raise NodeAlreadyChangedError("Node is already marked to "
+                    "be changed at %s" % node.path)
+            # We only mark node as *removed* - real removal is done by
+            # commit method
+            self.removed.append(node)
+
+    def reset(self):
+        """
+        Resets this instance to initial state (cleans ``added``, ``changed``
+        and ``removed`` lists).
+        """
+        self.added = []
+        self.changed = []
+        self.removed = []
+        self.parents = []
+
+    def get_ipaths(self):
+        """
+        Returns generator of paths from nodes marked as added, changed or
+        removed.
+        """
+        for node in chain(self.added, self.changed, self.removed):
+            yield node.path
+
+    def get_paths(self):
+        """
+        Returns list of paths from nodes marked as added, changed or removed.
+        """
+        return list(self.get_ipaths())
+
+    def check_integrity(self, parents=None):
+        """
+        Checks in-memory changeset's integrity. Also, sets parents if not
+        already set.
+
+        :raises CommitError: if any error occurs (i.e.
+          ``NodeDoesNotExistError``).
+        """
+        if not self.parents:
+            parents = parents or []
+            if len(parents) == 0:
+                try:
+                    parents = [self.repository.get_changeset(), None]
+                except EmptyRepositoryError:
+                    parents = [None, None]
+            elif len(parents) == 1:
+                parents += [None]
+            self.parents = parents
+
+        # Local parents, only if not None
+        parents = [p for p in self.parents if p]
+
+        # Check nodes marked as added
+        for p in parents:
+            for node in self.added:
+                try:
+                    p.get_node(node.path)
+                except NodeDoesNotExistError:
+                    pass
+                else:
+                    raise NodeAlreadyExistsError("Node at %s already exists "
+                        "at %s" % (node.path, p))
+
+        # Check nodes marked as changed
+        missing = set(self.changed)
+        not_changed = set(self.changed)
+        if self.changed and not parents:
+            raise NodeDoesNotExistError(str(self.changed[0].path))
+        for p in parents:
+            for node in self.changed:
+                try:
+                    old = p.get_node(node.path)
+                    missing.remove(node)
+                    if old.content != node.content:
+                        not_changed.remove(node)
+                except NodeDoesNotExistError:
+                    pass
+        if self.changed and missing:
+            raise NodeDoesNotExistError("Node at %s is missing "
+                "(parents: %s)" % (node.path, parents))
+
+        if self.changed and not_changed:
+            raise NodeNotChangedError("Node at %s wasn't actually changed "
+                "since parents' changesets: %s" % (not_changed.pop().path,
+                    parents)
+            )
+
+        # Check nodes marked as removed
+        if self.removed and not parents:
+            raise NodeDoesNotExistError("Cannot remove node at %s as there "
+                "were no parents specified" % self.removed[0].path)
+        really_removed = set()
+        for p in parents:
+            for node in self.removed:
+                try:
+                    p.get_node(node.path)
+                    really_removed.add(node)
+                except ChangesetError:
+                    pass
+        not_removed = set(self.removed) - really_removed
+        if not_removed:
+            raise NodeDoesNotExistError("Cannot remove node at %s from "
+                "following parents: %s" % (not_removed[0], parents))
+
+    def commit(self, message, author, parents=None, branch=None, date=None,
+            **kwargs):
+        """
+        Performs in-memory commit (doesn't check workdir in any way) and
+        returns newly created ``Changeset``. Updates repository's
+        ``revisions``.
+
+        .. note::
+            While overriding this method each backend's should call
+            ``self.check_integrity(parents)`` in the first place.
+
+        :param message: message of the commit
+        :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
+        :param parents: single parent or sequence of parents from which commit
+          would be derieved
+        :param date: ``datetime.datetime`` instance. Defaults to
+          ``datetime.datetime.now()``.
+        :param branch: branch name, as string. If none given, default backend's
+          branch would be used.
+
+        :raises ``CommitError``: if any error occurs while committing
+        """
+        raise NotImplementedError

File rhodecode/lib/vcs/backends/git/__init__.py

+from .repository import GitRepository
+from .changeset import GitChangeset
+from .inmemory import GitInMemoryChangeset
+from .workdir import GitWorkdir
+
+
+__all__ = [
+    'GitRepository', 'GitChangeset', 'GitInMemoryChangeset', 'GitWorkdir',
+]

File rhodecode/lib/vcs/backends/git/changeset.py

+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.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
+
+        try:
+            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)
+
+    @LazyProperty
+    def date(self):
+        return date_fromtimestamp(self._commit.commit_time,
+                                  self._commit.commit_timezone)
+
+    @LazyProperty
+    def status(self):
+        """
+        Returns modified, added, removed, deleted files for current changeset
+        """
+        return self.changed, self.added, self.removed
+
+    @LazyProperty
+    def branch(self):
+        # TODO: Cache as we walk (id <-> branch name mapping)
+        refs = self.repository._repo.get_refs()
+        heads = [(key[len('refs/heads/'):], val) for key, val in refs.items()
+            if key.startswith('refs/heads/')]
+
+        for name, id in heads:
+            walker = self.repository._repo.object_store.get_graph_walker([id])
+            while True:
+                id = walker.next()
+                if not id:
+                    break
+                if id == self.id:
+                    return safe_unicode(name)
+        raise ChangesetError("This should not happen... Have you manually "
+            "change id of the changeset?")
+
+    def _fix_path(self, path):
+        """
+        Paths are stored without trailing slash so we need to get rid off it if
+        needed.
+        """
+        if path.endswith('/'):
+            path = path.rstrip('/')
+        return path
+
+    def _get_id_for_path(self, path):
+        # FIXME: Please, spare a couple of minutes and make those codes cleaner;
+        if not path in self._paths:
+            path = path.strip('/')
+            # set root tree
+            tree = self.repository._repo[self._commit.tree]
+            if path == '':
+                self._paths[''] = tree.id
+                return tree.id
+            splitted = path.split('/')
+            dirs, name = splitted[:-1], splitted[-1]
+            curdir = ''
+            for dir in dirs:
+                if curdir:
+                    curdir = '/'.join((curdir, dir))
+                else:
+                    curdir = dir
+                #if curdir in self._paths:
+                    ## This path have been already traversed
+                    ## Update tree and continue
+                    #tree = self.repository._repo[self._paths[curdir]]
+                    #continue
+                dir_id = None
+                for item, stat, id in tree.iteritems():
+                    if curdir:
+                        item_path = '/'.join((curdir, item))
+                    else:
+                        item_path = item
+                    self._paths[item_path] = id
+                    self._stat_modes[item_path] = stat
+                    if dir == item:
+                        dir_id = id
+                if dir_id:
+                    # Update tree
+                    tree = self.repository._repo[dir_id]
+                    if not isinstance(tree, objects.Tree):
+                        raise ChangesetError('%s is not a directory' % curdir)
+                else:
+                    raise ChangesetError('%s have not been found' % curdir)
+            for item, stat, id in tree.iteritems():
+                if curdir:
+                    name = '/'.join((curdir, item))
+                else:
+                    name = item
+                self._paths[name] = id
+                self._stat_modes[name] = stat
+            if not path in self._paths:
+                raise NodeDoesNotExistError("There is no file nor directory "
+                    "at the given path %r at revision %r"
+                    % (path, self.short_id))
+        return self._paths[path]
+
+    def _get_kind(self, path):
+        id = self._get_id_for_path(path)
+        obj = self.repository._repo[id]
+        if isinstance(obj, objects.Blob):
+            return NodeKind.FILE
+        elif isinstance(obj, objects.Tree):
+            return NodeKind.DIR
+
+    def _get_file_nodes(self):
+        return chain(*(t[2] for t in self.walk()))
+
+    @LazyProperty
+    def parents(self):
+        """
+        Returns list of parents changesets.
+        """
+        return [self.repository.get_changeset(parent)
+            for parent in self._commit.parents]
+
+    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 get_file_mode(self, path):
+        """
+        Returns stat mode of the file at the given ``path``.
+        """
+        # ensure path is traversed
+        self._get_id_for_path(path)
+        return self._stat_modes[path]
+
+    def get_file_content(self, path):
+        """
+        Returns content of the file at given ``path``.
+        """
+        id = self._get_id_for_path(path)
+        blob = self.repository._repo[id]
+        return blob.as_pretty_string()
+
+    def get_file_size(self, path):
+        """
+        Returns size of the file at given ``path``.
+        """
+        id = self._get_id_for_path(path)
+        blob = self.repository._repo[id]
+        return blob.raw_length()
+
+    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.
+
+        TODO: This function now uses os underlying 'git' and 'grep' commands
+        which is generally not good. Should be replaced with algorithm
+        iterating commits.
+        """
+        cmd = 'log --name-status -p %s -- "%s" | grep "^commit"' \
+            % (self.id, path)
+        so, se = self.repository.run_git_command(cmd)
+        ids = re.findall(r'\w{40}', so)
+        return [self.repository.get_changeset(id) for id in ids]
+
+    def get_file_annotate(self, path):
+        """
+        Returns a list of three element tuples with lineno,changeset and line
+
+        TODO: This function now uses os underlying 'git' command which is
+        generally not good. Should be replaced with algorithm iterating
+        commits.
+        """
+        cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
+        # -l     ==> outputs long shas (and we need all 40 characters)
+        # --root ==> doesn't put '^' character for bounderies
+        # -r sha ==> blames for the given revision
+        so, se = self.repository.run_git_command(cmd)
+        annotate = []
+        for i, blame_line in enumerate(so.split('\n')[:-1]):
+            ln_no = i + 1
+            id, line = re.split(r' \(.+?\) ', blame_line, 1)
+            annotate.append((ln_no, self.repository.get_changeset(id), line))
+        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 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)
+            chunk = popen.stdout.read(buffer_size)
+        # 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 = []
+        for name, stat, id in tree.iteritems():
+            obj = self.repository._repo.get_object(id)
+            if path != '':
+                obj_path = '/'.join((path, name))
+            else:
+                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))
+            else:
+                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')
+        path = self._fix_path(path)
+        if not path in self.nodes:
+            try:
+                id = self._get_id_for_path(path)
+            except ChangesetError:
+                raise NodeDoesNotExistError("Cannot find one of parents' "
+                    "directories for a given path: %s" % path)
+            obj = self.repository._repo.get_object(id)
+            if isinstance(obj, objects.Tree):
+                if path == '':
+                    node = RootNode(changeset=self)
+                else:
+                    node = DirNode(path, changeset=self)
+                node._tree = obj
+            elif isinstance(obj, objects.Blob):
+                node = FileNode(path, changeset=self)
+                node._blob = obj
+            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.added + self.changed
+
+    @LazyProperty
+    def _diff_name_status(self):
+        output = []
+        for parent in self.parents:
+            cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
+            so, se = self.repository.run_git_command(cmd)
+            output.append(so.strip())
+        return '\n'.join(output)
+
+    def _get_paths_for_status(self, status):
+        """
+        Returns sorted list of paths for given ``status``.
+
+        :param status: one of: *added*, *modified* or *deleted*
+        """
+        paths = set()
+        char = status[0].upper()
+        for line in self._diff_name_status.splitlines():
+            if not line:
+                continue
+            if line.startswith(char):
+                splitted = line.split(char,1)
+                if not len(splitted) == 2:
+                    raise VCSError("Couldn't parse diff result:\n%s\n\n and "
+                        "particularly that line: %s" % (self._diff_name_status,
+                        line))
+                paths.add(splitted[1].strip())
+        return sorted(paths)
+
+    @LazyProperty
+    def added(self):
+        """
+        Returns list of added ``FileNode`` objects.
+        """
+        if not self.parents:
+            return list(self._get_file_nodes())
+        return [self.get_node(path) for path in self._get_paths_for_status('added')]
+
+    @LazyProperty
+    def changed(self):
+        """
+        Returns list of modified ``FileNode`` objects.
+        """
+        if not self.parents:
+            return []
+        return [self.get_node(path) for path in self._get_paths_for_status('modified')]
+
+    @LazyProperty
+    def removed(self):
+        """
+        Returns list of removed ``FileNode`` objects.
+        """
+        if not self.parents:
+            return []
+        return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]

File rhodecode/lib/vcs/backends/git/config.py

+# config.py - Reading and writing Git config files
+# Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) a later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Reading and writing Git configuration files.
+
+TODO:
+ * preserve formatting when updating configuration files
+ * treat subsection names as case-insensitive for [branch.foo] style
+   subsections
+"""
+
+# Taken from dulwich not yet released 0.8.3 version (until it is actually
+# released)
+
+import errno
+import os
+import re
+
+from dulwich.file import GitFile
+
+
+class Config(object):
+    """A Git configuration."""
+
+    def get(self, section, name):
+        """Retrieve the contents of a configuration setting.
+
+        :param section: Tuple with section name and optional subsection namee
+        :param subsection: Subsection name
+        :return: Contents of the setting
+        :raise KeyError: if the value is not set
+        """
+        raise NotImplementedError(self.get)
+
+    def get_boolean(self, section, name, default=None):
+        """Retrieve a configuration setting as boolean.
+
+        :param section: Tuple with section name and optional subsection namee
+        :param name: Name of the setting, including section and possible
+            subsection.
+        :return: Contents of the setting
+        :raise KeyError: if the value is not set
+        """
+        try:
+            value = self.get(section, name)
+        except KeyError:
+            return default
+        if value.lower() == "true":
+            return True
+        elif value.lower() == "false":
+            return False
+        raise ValueError("not a valid boolean string: %r" % value)
+
+    def set(self, section, name, value):
+        """Set a configuration value.
+
+        :param name: Name of the configuration value, including section
+            and optional subsection
+        :param: Value of the setting
+        """
+        raise NotImplementedError(self.set)
+
+
+class ConfigDict(Config):
+    """Git configuration stored in a dictionary."""
+
+    def __init__(self, values=None):
+        """Create a new ConfigDict."""
+        if values is None:
+            values = {}
+        self._values = values
+
+    def __repr__(self):
+        return "%s(%r)" % (self.__class__.__name__, self._values)
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__) and
+            other._values == self._values)
+
+    @classmethod
+    def _parse_setting(cls, name):
+        parts = name.split(".")
+        if len(parts) == 3:
+            return (parts[0], parts[1], parts[2])
+        else:
+            return (parts[0], None, parts[1])
+
+    def get(self, section, name):
+        if isinstance(section, basestring):
+            section = (section, )
+        if len(section) > 1:
+            try:
+                return self._values[section][name]
+            except KeyError:
+                pass
+        return self._values[(section[0],)][name]
+
+    def set(self, section, name, value):
+        if isinstance(section, basestring):
+            section = (section, )
+        self._values.setdefault(section, {})[name] = value
+
+
+def _format_string(value):
+    if (value.startswith(" ") or
+        value.startswith("\t") or
+        value.endswith(" ") or
+        value.endswith("\t")):
+        return '"%s"' % _escape_value(value)
+    return _escape_value(value)
+
+
+def _parse_string(value):
+    value = value.strip()
+    ret = []
+    block = []
+    in_quotes = False
+    for c in value:
+        if c == "\"":