summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorhut <hut@lepus.uberspace.de>2016-02-28 22:06:28 +0100
committerhut <hut@lepus.uberspace.de>2016-02-28 22:06:28 +0100
commit2de7ea308fb88f34a181de34c35c1f18fbd5c7aa (patch)
tree42316333b0c9a5f95f15ddf8ca3140c42e7ce5cf
parentcb2b20166657d0716f4aefd777fa8f7dd80e8d3e (diff)
parent4b9a296eeea704bd8adc12d06ef27d7b01be1c75 (diff)
downloadranger-2de7ea308fb88f34a181de34c35c1f18fbd5c7aa.tar.gz
Merge branch 'master' of https://github.com/nfnty/ranger
-rw-r--r--AUTHORS2
-rw-r--r--ranger/colorschemes/default.py5
-rwxr-xr-xranger/config/commands.py137
-rw-r--r--ranger/container/directory.py53
-rw-r--r--ranger/container/fsobject.py90
-rw-r--r--ranger/ext/vcs/__init__.py10
-rw-r--r--ranger/ext/vcs/bzr.py335
-rw-r--r--ranger/ext/vcs/git.py414
-rw-r--r--ranger/ext/vcs/hg.py358
-rw-r--r--ranger/ext/vcs/svn.py352
-rw-r--r--ranger/ext/vcs/vcs.py732
-rw-r--r--ranger/gui/context.py4
-rw-r--r--ranger/gui/ui.py15
-rw-r--r--ranger/gui/widgets/__init__.py36
-rw-r--r--ranger/gui/widgets/browsercolumn.py40
-rw-r--r--ranger/gui/widgets/statusbar.py36
16 files changed, 1073 insertions, 1546 deletions
diff --git a/AUTHORS b/AUTHORS
index ec17affb..d3789119 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -21,6 +21,8 @@ Copyright 2015  Delisa Mason <iskanamagus@gmail.com>
 Copyright 2015  No Suck <admin@nosuck.org>
 Copyright 2015  Randy Nance <randynobx@gmail.com>
 Copyright 2015  Wojciech Siewierski <wojciech.siewierski@onet.pl>
+Copyright 2015  Ryan Burns <rdburns@gmail.com>
+Copyright 2015  nfnty <git@nfnty.se>
 
 Ideally, all contributors of non-trivial code are named here to the extent that
 a name and e-mail address is available.  Please write a mail to hut@hut.pm if
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index 80fa9e39..e5dbf3a6 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -100,6 +100,9 @@ class Default(ColorScheme):
             if context.vcscommit:
                 fg = yellow
                 attr &= ~bold
+            if context.vcsdate:
+                fg = cyan
+                attr &= ~bold
 
 
         if context.text:
@@ -137,7 +140,7 @@ class Default(ColorScheme):
 
         elif context.vcsremote and not context.selected:
             attr &= ~bold
-            if context.vcssync:
+            if context.vcssync or context.vcsnone:
                 fg = green
             elif context.vcsbehind:
                 fg = red
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index f734d64a..3e7af7b5 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -1320,127 +1320,68 @@ class grep(Command):
             action.extend(f.path for f in self.fm.thistab.get_selection())
             self.fm.execute_command(action, flags='p')
 
-
-# Version control commands
-# --------------------------------
-class stage(Command):
-    """
-    :stage
-
-    Stage selected files for the corresponding version control system
+class flat(Command):
     """
-    def execute(self):
-        from ranger.ext.vcs import VcsError
-
-        filelist = [f.path for f in self.fm.thistab.get_selection()]
-        self.fm.thisdir.vcs_outdated = True
-#        for f in self.fm.thistab.get_selection():
-#            f.vcs_outdated = True
-
-        try:
-            self.fm.thisdir.vcs.add(filelist)
-        except VcsError:
-            self.fm.notify("Could not stage files.")
-
-        self.fm.reload_cwd()
+    :flat <level>
 
+    Flattens the directory view up to the specified level.
 
-class unstage(Command):
+        -1 fully flattened
+         0 remove flattened view
     """
-    :unstage
 
-    Unstage selected files for the corresponding version control system
-    """
     def execute(self):
-        from ranger.ext.vcs import VcsError
-
-        filelist = [f.path for f in self.fm.thistab.get_selection()]
-        self.fm.thisdir.vcs_outdated = True
-#        for f in self.fm.thistab.get_selection():
-#            f.vcs_outdated = True
-
         try:
-            self.fm.thisdir.vcs.reset(filelist)
-        except VcsError:
-            self.fm.notify("Could not unstage files.")
-
-        self.fm.reload_cwd()
+            level = self.rest(1)
+            level = int(level)
+        except ValueError:
+            level = self.quantifier
+        if level < -1:
+            self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
+        self.fm.thisdir.unload()
+        self.fm.thisdir.flat = level
+        self.fm.thisdir.load_content()
 
+# Version control commands
+# --------------------------------
 
-class diff(Command):
+class stage(Command):
     """
-    :diff
+    :stage
 
-    Displays a diff of selected files against the last committed version
+    Stage selected files for the corresponding version control system
     """
     def execute(self):
         from ranger.ext.vcs import VcsError
-        import tempfile
-
-        L = self.fm.thistab.get_selection()
-        if len(L) == 0: return
-
-        filelist = [f.path for f in L]
-        vcs = L[0].vcs
-
-        diff = vcs.get_raw_diff(filelist=filelist)
-        if len(diff.strip()) > 0:
-            tmp = tempfile.NamedTemporaryFile()
-            tmp.write(diff.encode('utf-8'))
-            tmp.flush()
 
-            pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER)
-            self.fm.run([pager, tmp.name])
+        if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
+            filelist = [f.path for f in self.fm.thistab.get_selection()]
+            try:
+                self.fm.thisdir.vcs.action_add(filelist)
+            except VcsError as error:
+                self.fm.notify('Unable to stage files: {0:s}'.format(str(error)))
+            self.fm.ui.vcsthread.wakeup(self.fm.thisdir)
         else:
-            raise Exception("diff is empty")
-
+            self.fm.notify('Unable to stage files: Not in repository')
 
-class log(Command):
+class unstage(Command):
     """
-    :log
+    :unstage
 
-    Displays the log of the current repo or files
+    Unstage selected files for the corresponding version control system
     """
     def execute(self):
         from ranger.ext.vcs import VcsError
-        import tempfile
-
-        L = self.fm.thistab.get_selection()
-        if len(L) == 0: return
-
-        filelist = [f.path for f in L]
-        vcs = L[0].vcs
-
-        log = vcs.get_raw_log(filelist=filelist)
-        tmp = tempfile.NamedTemporaryFile()
-        tmp.write(log.encode('utf-8'))
-        tmp.flush()
-
-        pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER)
-        self.fm.run([pager, tmp.name])
-
-class flat(Command):
-    """
-    :flat <level>
-
-    Flattens the directory view up to the specified level.
-
-        -1 fully flattened
-         0 remove flattened view
-    """
-
-    def execute(self):
-        try:
-            level = self.rest(1)
-            level = int(level)
-        except ValueError:
-            level = self.quantifier
-        if level < -1:
-            self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
-        self.fm.thisdir.unload()
-        self.fm.thisdir.flat = level
-        self.fm.thisdir.load_content()
 
+        if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
+            filelist = [f.path for f in self.fm.thistab.get_selection()]
+            try:
+                self.fm.thisdir.vcs.action_reset(filelist)
+            except VcsError as error:
+                self.fm.notify('Unable to unstage files: {0:s}'.format(str(error)))
+            self.fm.ui.vcsthread.wakeup(self.fm.thisdir)
+        else:
+            self.fm.notify('Unable to unstage files: Not in repository')
 
 # Metadata commands
 # --------------------------------
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 52b494d5..8ace0d85 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -18,6 +18,7 @@ from ranger.ext.accumulator import Accumulator
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.human_readable import human_readable
 from ranger.container.settings import LocalSettings
+from ranger.ext.vcs import Vcs
 
 def sort_by_basename(path):
     """returns path.relative_path (for sorting)"""
@@ -109,6 +110,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
     content_outdated = False
     content_loaded = False
 
+    vcs = None
     has_vcschild = False
 
     _cumulative_size_calculated = False
@@ -145,6 +147,9 @@ class Directory(FileSystemObject, Accumulator, Loadable):
 
         self.settings = LocalSettings(path, self.settings)
 
+        if self.settings.vcs_aware:
+            self.vcs = Vcs(self)
+
         self.use()
 
     def request_resort(self):
@@ -229,7 +234,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
             filters.append(lambda file: temporary_filter_search(file.basename))
 
         self.files = [f for f in self.files_all if accept_file(f, filters)]
-        
+
         # A fix for corner cases when the user invokes show_hidden on a
         # directory that contains only hidden directories and hidden files.
         if self.files and not self.pointed_obj:
@@ -301,10 +306,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                 files = []
                 disk_usage = 0
 
-                if self.settings.vcs_aware:
-                    self.has_vcschild = False
-                    self.load_vcs(None)
-
+                has_vcschild = False
                 for name in filenames:
                     try:
                         file_lstat = os_lstat(name)
@@ -318,34 +320,39 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                         stats = None
                         is_a_dir = False
                     if is_a_dir:
-                        if self.flat:
+                        try:
+                            item = self.fm.get_directory(name)
+                            item.load_if_outdated()
+                        except:
                             item = Directory(name, preload=stats, path_is_abs=True,
-                                    basename_is_rel_to=basename_is_rel_to)
+                                             basename_is_rel_to=basename_is_rel_to)
                             item.load()
                         else:
-                            try:
-                                item = self.fm.get_directory(name)
-                                item.load_if_outdated()
-                            except:
-                                item = Directory(name, preload=stats, path_is_abs=True)
-                                item.load()
+                            if self.flat:
+                                item.relative_path = os.path.relpath(item.path, self.path)
+                            else:
+                                item.relative_path = item.basename
+                            item.relative_path_lower = item.relative_path.lower()
+                        if item.vcs and item.vcs.track:
+                            if item.vcs.is_root_pointer:
+                                has_vcschild = True
+                            else:
+                                item.vcsstatus = item.vcs.rootvcs.status_subpath(
+                                    os.path.join(self.realpath, item.basename), is_directory=True)
                     else:
                         item = File(name, preload=stats, path_is_abs=True,
                                     basename_is_rel_to=basename_is_rel_to)
                         item.load()
                         disk_usage += item.size
-
-                    # Load vcs data
-                    if self.settings.vcs_aware:
-                        item.load_vcs(self)
-                        if item.vcs_enabled:
-                            self.has_vcschild = True
+                        if self.vcs and self.vcs.track:
+                            item.vcsstatus = self.vcs.rootvcs.status_subpath(
+                                os.path.join(self.realpath, item.basename))
 
                     files.append(item)
                     self.percent = 100 * len(files) // len(filenames)
                     yield
+                self.has_vcschild = has_vcschild
                 self.disk_usage = disk_usage
-                self.vcs_outdated = False
 
                 self.filenames = filenames
                 self.files_all = files
@@ -378,6 +385,8 @@ class Directory(FileSystemObject, Accumulator, Loadable):
         finally:
             self.loading = False
             self.fm.signal_emit("finished_loading_dir", directory=self)
+            if self.vcs:
+                self.fm.ui.vcsthread.wakeup(self)
 
     def unload(self):
         self.loading = False
@@ -416,7 +425,6 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                     pass
                 self.load_generator = None
 
-
     def sort(self):
         """Sort the contained files"""
         if self.files_all is None:
@@ -582,7 +590,8 @@ class Directory(FileSystemObject, Accumulator, Loadable):
     def load_content_if_outdated(self, *a, **k):
         """Load the contents of the directory if outdated"""
 
-        if self.load_content_once(*a, **k): return True
+        if self.load_content_once(*a, **k):
+            return True
 
         if self.files_all is None or self.content_outdated:
             self.load_content(*a, **k)
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index aa848b7a..1daf6d70 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -73,16 +73,8 @@ class FileSystemObject(FileManagerAware, SettingsAware):
 
     size = 0
 
-    (vcs,
-     vcsfilestatus,
-     vcsremotestatus,
-     vcsbranch,
-     vcshead) = (None,) * 5
-
-    vcs_outdated = False
-    vcs_enabled = False
-
-    basename_is_rel_to = None
+    vcsstatus = None
+    vcsremotestatus = None
 
     _linemode = DEFAULT_LINEMODE
     linemode_dict = dict(
@@ -94,12 +86,10 @@ class FileSystemObject(FileManagerAware, SettingsAware):
         if not path_is_abs:
             path = abspath(path)
         self.path = path
-        self.basename_is_rel_to = basename_is_rel_to
+        self.basename = basename(path)
         if basename_is_rel_to == None:
-            self.basename = basename(path)
             self.relative_path = self.basename
         else:
-            self.basename = basename(path)
             self.relative_path = relpath(path, basename_is_rel_to)
         self.relative_path_lower = self.relative_path.lower()
         self.extension = splitext(self.basename)[1].lstrip(extsep) or None
@@ -241,78 +231,6 @@ class FileSystemObject(FileManagerAware, SettingsAware):
                 return None  # it is impossible to get the link destination
         return self.path
 
-    def load_vcs(self, parent):
-        """
-        Reads data regarding the version control system the object is on.
-        Does not load content specific data.
-        """
-        from ranger.ext.vcs import Vcs, VcsError
-
-        vcs = Vcs(self.path)
-
-        # Not under vcs
-        if vcs.root == None:
-            return
-
-        # Already know about the right vcs
-        elif self.vcs and abspath(vcs.root) == abspath(self.vcs.root):
-            self.vcs.update()
-
-        # Need new Vcs object and self.path is the root
-        elif self.vcs == None and abspath(vcs.root) == abspath(self.path):
-            self.vcs = vcs
-            self.vcs_outdated = True
-
-        # Otherwise, find the root, and try to get the Vcs object from there
-        else:
-            rootdir = self.fm.get_directory(vcs.root)
-            rootdir.load_if_outdated()
-
-            # Get the Vcs object from rootdir
-            rootdir.load_vcs(None)
-            self.vcs = rootdir.vcs
-            if rootdir.vcs_outdated:
-                self.vcs_outdated = True
-
-        if self.vcs:
-            if self.vcs.vcsname == 'git':
-                backend_state = self.settings.vcs_backend_git
-            elif self.vcs.vcsname == 'hg':
-                backend_state = self.settings.vcs_backend_hg
-            elif self.vcs.vcsname == 'bzr':
-                backend_state = self.settings.vcs_backend_bzr
-            elif self.vcs.vcsname == 'svn':
-                backend_state = self.settings.vcs_backend_svn
-            else:
-                backend_state = 'disabled'
-
-            self.vcs_enabled = backend_state in set(['enabled', 'local'])
-            if self.vcs_enabled:
-                try:
-                    if self.vcs_outdated or (parent and parent.vcs_outdated):
-                        self.vcs_outdated = False
-                        # this caches the file status for get_file_status():
-                        self.vcs.get_status()
-                        self.vcsbranch = self.vcs.get_branch()
-                        self.vcshead = self.vcs.get_info(self.vcs.HEAD)
-                        if self.path == self.vcs.root and \
-                                backend_state == 'enabled':
-                            self.vcsremotestatus = \
-                                    self.vcs.get_remote_status()
-                    elif parent:
-                        self.vcsbranch = parent.vcsbranch
-                        self.vcshead = parent.vcshead
-                    self.vcsfilestatus = self.vcs.get_file_status(self.path)
-                except VcsError as err:
-                    self.vcsbranch = None
-                    self.vcshead = None
-                    self.vcsremotestatus = 'unknown'
-                    self.vcsfilestatus = 'unknown'
-                    self.fm.notify("Can not load vcs data on %s: %s" %
-                            (self.path, err), bad=True)
-        else:
-            self.vcs_enabled = False
-
     def load(self):
         """Loads information about the directory itself.
 
@@ -411,8 +329,6 @@ class FileSystemObject(FileManagerAware, SettingsAware):
             real_ctime = None
         if not self.stat or self.stat.st_ctime != real_ctime:
             self.load()
-            if self.settings.vcs_aware:
-                self.vcs_outdated = True
             return True
         return False
 
diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py
index 3f8e8138..26a32800 100644
--- a/ranger/ext/vcs/__init__.py
+++ b/ranger/ext/vcs/__init__.py
@@ -1,12 +1,8 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011
-#
-# vcs - a python module to handle various version control systems
 
-import os
+"""VCS Extension"""
 
-from .vcs import VcsError, Vcs
+from .vcs import Vcs, VcsError, VcsThread
 
-# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
+__all__ = ['Vcs', 'VcsError', 'VcsThread']
diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py
index 2a52cf02..5decc8b1 100644
--- a/ranger/ext/vcs/bzr.py
+++ b/ranger/ext/vcs/bzr.py
@@ -1,270 +1,141 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2012
-#
-# vcs - a python module to handle various version control systems
 
+"""GNU Bazaar module"""
+
+from datetime import datetime
 import os
 import re
-import shutil
-from datetime import datetime
 
 from .vcs import Vcs, VcsError
 
 
 class Bzr(Vcs):
-    vcsname  = 'bzr'
-    HEAD="last:1"
-
-    # Auxiliar stuff
-    #---------------------------
-
-    def _bzr(self, path, args, silent=True, catchout=False, bytes=False):
-        return self._vcs(path, 'bzr', args, silent=silent, catchout=catchout, bytes=bytes)
-
-
-    def _has_head(self):
-        """Checks whether repo has head"""
-        rnum = self._bzr(self.path, ['revno'], catchout=True)
-        return rnum != '0'
-
+    """VCS implementation for GNU Bazaar"""
+    HEAD = 'last:1'
 
-    def _sanitize_rev(self, rev):
-        if rev == None: return None
-        rev = rev.strip()
-        if len(rev) == 0: return None
+    _status_translations = (
+        ('+ -R', 'K NM', 'staged'),
+        (' -', 'D', 'deleted'),
+        ('?', ' ', 'untracked'),
+    )
 
-        return rev
+    # Generic
 
+    def _remote_url(self):
+        """Remote url"""
+        try:
+            return self._run(['config', 'parent_location']).rstrip('\n') or None
+        except VcsError:
+            return None
 
     def _log(self, refspec=None, filelist=None):
-        """Gets a list of dicts containing revision info, for the revisions matching refspec"""
-        args = ['log', '-n0', '--show-ids']
-        if refspec: args = args + ["-r", refspec]
+        """Returns an array of dicts containing revision info for refspec"""
+        args = ['log', '--log-format', 'long', '--levels', '0', '--show-ids']
+        if refspec:
+            args += ['--revision', refspec]
+        if filelist:
+            args += ['--'] + filelist
 
-        if filelist: args = args + filelist
-
-        raw = self._bzr(self.path, args, catchout=True, silent=True)
-        L = re.findall('-+$(.*?)^-', raw + '\n---', re.MULTILINE | re.DOTALL)
+        try:
+            output = self._run(args)
+        except VcsError:
+            return None
+        entries = re.findall(r'-+\n(.+?)\n(?:-|\Z)', output, re.MULTILINE | re.DOTALL)
 
         log = []
-        for t in L:
-            t = t.strip()
-            if len(t) == 0: continue
-
-            dt = {}
-            m = re.search('^revno:\s*([0-9]+)\s*$', t, re.MULTILINE)
-            if m: dt['short'] = m.group(1).strip()
-            m = re.search('^revision-id:\s*(.+)\s*$', t, re.MULTILINE)
-            if m: dt['revid'] = m.group(1).strip()
-            m = re.search('^committer:\s*(.+)\s*$', t, re.MULTILINE)
-            if m: dt['author'] = m.group(1).strip()
-            m = re.search('^timestamp:\s*(.+)\s*$', t, re.MULTILINE)
-            if m: dt['date'] = datetime.strptime(m.group(1).strip(), '%a %Y-%m-%d %H:%M:%S %z')
-            m = re.search('^message:\s*^(.+)$', t, re.MULTILINE)
-            if m: dt['summary'] = m.group(1).strip()
-            log.append(dt)
+        for entry in entries:
+            new = {}
+            try:
+                new['short'] = re.search(r'^revno: ([0-9]+)', entry, re.MULTILINE).group(1)
+                new['revid'] = re.search(r'^revision-id: (.+)$', entry, re.MULTILINE).group(1)
+                new['author'] = re.search(r'^committer: (.+)$', entry, re.MULTILINE).group(1)
+                new['date'] = datetime.strptime(
+                    re.search(r'^timestamp: (.+)$', entry, re.MULTILINE).group(1),
+                    '%a %Y-%m-%d %H:%M:%S %z'
+                )
+                new['summary'] = re.search(r'^message:\n  (.+)$', entry, re.MULTILINE).group(1)
+            except AttributeError:
+                return None
+            log.append(new)
         return log
 
-
-    def _bzr_file_status(self, st):
-        st = st.strip()
-        if   st in "AM":     return 'staged'
-        elif st in "D":      return 'deleted'
-        elif st in "?":      return 'untracked'
-        else:                return 'unknown'
-
-
-
-    # Repo creation
-    #---------------------------
-
-    def init(self):
-        """Initializes a repo in current path"""
-        self._bzr(self.path, ['init'])
-        self.update()
-
-
-    def clone(self, src):
-        """Clones a repo from src"""
-        path = os.path.dirname(self.path)
-        name = os.path.basename(self.path)
-        try:
-            os.rmdir(self.path)
-        except OSError:
-            raise VcsError("Can't clone to %s. It is not an empty directory" % self.path)
-
-        self._bzr(path, ['branch', src, name])
-        self.update()
-
-
+    def _status_translate(self, code):
+        """Translate status code"""
+        for code_x, code_y, status in self._status_translations:
+            if code[0] in code_x and code[1] in code_y:
+                return status
+        return 'unknown'
 
     # Action Interface
-    #---------------------------
-
-    def commit(self, message):
-        """Commits with a given message"""
-        self._bzr(self.path, ['commit', '-m', message])
-
-
-    def add(self, filelist=None):
-        """Adds files to the index, preparing for commit"""
-        if filelist != None: self._bzr(self.path, ['add'] + filelist)
-        else:                self._bzr(self.path, ['add'])
-
-
-    def reset(self, filelist=None):
-        """Removes files from the index"""
-        if filelist != None: self._bzr(self.path, ['remove', '--keep', '--new'] + filelist)
-        else:                self._bzr(self.path, ['remove', '--keep', '--new'])
-
-
-    def pull(self):
-        """Pulls a git repo"""
-        self._bzr(self.path, ['pull'])
-
-
-    def push(self):
-        """Pushes a git repo"""
-        self._bzr(self.path, ['push'])
-
-
-    def checkout(self, rev):
-        """Checks out a branch or revision"""
-        self._bzr(self.path, ['update', '-r', rev])
-
-
-    def extract_file(self, rev, name, dest):
-        """Extracts a file from a given revision and stores it in dest dir"""
-        if rev == self.INDEX:
-            shutil.copyfile(os.path.join(self.path, name), dest)
-        else:
-            out = self._bzr(self.path, ['cat', '--r', rev, name], catchout=True, bytes=True)
-            with open(dest, 'wb') as fd: fd.write(out)
 
+    def action_add(self, filelist=None):
+        args = ['add']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
 
+    def action_reset(self, filelist=None):
+        args = ['remove', '--keep', '--new']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
 
     # Data Interface
-    #---------------------------
-
-    def get_status_allfiles(self):
-        """Returns a dict indexed by files not in sync their status as values.
-           Paths are given relative to the root. Strips trailing '/' from dirs."""
-        raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, bytes=True)
-        L = re.findall('^(..)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        ret = {}
-        for st, p in L:
-            sta = self._bzr_file_status(st)
-            ret[os.path.normpath(p.strip())] = sta
-        return ret
-
-
-    def get_ignore_allfiles(self):
-        """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs."""
-        raw = self._bzr(self.path, ['ls', '--ignored'], catchout=True)
-        return set(os.path.normpath(p) for p in raw.split('\n'))
-
-
-    # TODO: slow due to net access
-    def get_remote_status(self):
-        """Checks the status of the repo regarding sync state with remote branch"""
-        if self.get_remote() == None:
-            return "none"
-
-        ahead = behind = True
-        try:
-            self._bzr(self.path, ['missing', '--mine-only'], silent=True)
-        except:
-            ahead = False
-
-        try:
-            self._bzr(self.path, ['missing', '--theirs-only'], silent=True)
-        except:
-            behind = False
 
-        if       ahead and     behind: return "diverged"
-        elif     ahead and not behind: return "ahead"
-        elif not ahead and     behind: return "behind"
-        elif not ahead and not behind: return "sync"
+    def data_status_root(self):
+        statuses = set()
 
+        # Paths with status
+        output = self._run(['status', '--short', '--no-classify']).rstrip('\n')
+        if not output:
+            return 'sync'
+        for line in output.split('\n'):
+            statuses.add(self._status_translate(line[:2]))
 
-    def get_branch(self):
-        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
-        branch = self._bzr(self.path, ['nick'], catchout=True)
-        return branch or None
+        for status in self.DIRSTATUSES:
+            if status in statuses:
+                return status
+        return 'sync'
 
+    def data_status_subpaths(self):
+        statuses = {}
 
-    def get_log(self, filelist=None, maxres=None):
-        """Get the entire log for the current HEAD"""
-        if not self._has_head(): return []
-        return self._log(refspec=None, filelist=filelist)
+        # Ignored
+        output = self._run(['ls', '--null', '--ignored']).rstrip('\x00')
+        if output:
+            for path in output.split('\x00'):
+                statuses[path] = 'ignored'
 
+        # Paths with status
+        output = self._run(['status', '--short', '--no-classify']).rstrip('\n')
+        for line in output.split('\n'):
+            statuses[os.path.normpath(line[4:])] = self._status_translate(line[:2])
 
-    def get_raw_log(self, filelist=None):
-        """Gets the raw log as a string"""
-        if not self._has_head(): return []
-        args = ['log']
-        if filelist: args = args + filelist
-        return self._bzr(self.path, args, catchout=True)
+        return statuses
 
+    def data_status_remote(self):
+        if not self._remote_url():
+            return 'none'
+        return 'unknown'
 
-    def get_raw_diff(self, refspec=None, filelist=None):
-        """Gets the raw diff as a string"""
-        args = ['diff', '--git']
-        if refspec:  args = args + [refspec]
-        if filelist: args = args + filelist
-        return self._bzr(self.path, args, catchout=True)
-
-
-    def get_remote(self):
-        """Returns the url for the remote repo attached to head"""
-        try:
-            remote = self._bzr(self.path, ['config', 'parent_location'], catchout=True)
-        except VcsError:
-            remote = ""
-
-        return remote.strip() or None
-
-
-    def get_revision_id(self, rev=None):
-        """Get a canonical key for the revision rev"""
-        if rev == None: rev = self.HEAD
-        elif rev == self.INDEX: return None
-        rev = self._sanitize_rev(rev)
+    def data_branch(self):
         try:
-            L = self._log(refspec=rev)
+            return self._run(['nick']).rstrip('\n') or None
         except VcsError:
-            L = []
-        if len(L) == 0: return None
-        else:           return L[0]['revid']
-
-
-    def get_info(self, rev=None):
-        """Gets info about the given revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-        if rev == self.HEAD and not self._has_head(): return []
-
-        L = self._log(refspec=rev)
-        if len(L) == 0:
-            raise VcsError("Revision %s does not exist" % rev)
-        elif len(L) > 1:
-            raise VcsError("More than one instance of revision %s ?!?" % rev)
+            return None
+
+    def data_info(self, rev=None):
+        if rev is None:
+            rev = self.HEAD
+
+        log = self._log(refspec=rev)
+        if not log:
+            if rev == self.HEAD:
+                return None
+            else:
+                raise VcsError('Revision {0:s} does not exist'.format(rev))
+        elif len(log) == 1:
+            return log[0]
         else:
-            return L[0]
-
-
-    def get_files(self, rev=None):
-        """Gets a list of files in revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-
-        if rev:
-            if rev == self.INDEX:  raw = self._bzr(self.path, ["ls"], catchout=True)
-            else:                  raw = self._bzr(self.path, ['ls', '--R', '-V', '-r', rev], catchout=True)
-            return raw.split('\n')
-        else:
-            return []
-
-# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
+            raise VcsError('More than one instance of revision {0:s}'.format(rev))
diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py
index f4950822..8f4d9ff8 100644
--- a/ranger/ext/vcs/git.py
+++ b/ranger/ext/vcs/git.py
@@ -1,302 +1,194 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011-2012
-#
-# vcs - a python module to handle various version control systems
 
+"""Git module"""
+
+from datetime import datetime
+import json
 import os
 import re
-import shutil
-from datetime import datetime
 
 from .vcs import Vcs, VcsError
 
 
 class Git(Vcs):
-    vcsname  = 'git'
+    """VCS implementation for Git"""
+    _status_translations = (
+        ('MADRC', ' ', 'staged'),
+        (' MADRC', 'M', 'changed'),
+        (' MARC', 'D', 'deleted'),
 
-    # Auxiliar stuff
-    #---------------------------
+        ('D', 'DU', 'conflict'),
+        ('A', 'AU', 'conflict'),
+        ('U', 'ADU', 'conflict'),
 
-    def _git(self, path, args, silent=True, catchout=False, bytes=False):
-        return self._vcs(path, 'git', args, silent=silent, catchout=catchout, bytes=bytes)
-
-
-    def _has_head(self):
-        """Checks whether repo has head"""
-        try:
-            self._git(self.path, ['rev-parse', 'HEAD'], silent=True)
-        except VcsError:
-            return False
-        return True
+        ('?', '?', 'untracked'),
+        ('!', '!', 'ignored'),
+    )
 
+    # Generic
 
     def _head_ref(self):
-        """Gets HEAD's ref"""
-        ref = self._git(self.path, ['symbolic-ref', self.HEAD], catchout=True, silent=True)
-        return ref.strip() or None
-
+        """Returns HEAD reference"""
+        return self._run(['symbolic-ref', self.HEAD]).rstrip('\n') or None
 
     def _remote_ref(self, ref):
-        """Gets remote ref associated to given ref"""
-        if ref == None: return None
-        remote = self._git(self.path, ['for-each-ref', '--format=%(upstream)', ref], catchout=True, silent=True)
-        return remote.strip() or None
-
-
-    def _sanitize_rev(self, rev):
-        if rev == None: return None
-        return rev.strip()
-
+        """Returns remote reference associated to given ref"""
+        if ref is None:
+            return None
+        return self._run(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None
 
     def _log(self, refspec=None, maxres=None, filelist=None):
-        """Gets a list of dicts containing revision info, for the revisions matching refspec"""
-        fmt = '--pretty=%h %H%nAuthor: %an <%ae>%nDate: %ct%nSubject: %s%n'
-
-        args = ['--no-pager', 'log', fmt]
-        if refspec:  args = args + ['-1', refspec]
-        elif maxres: args = args + ['-%d' % maxres]
-
-        if filelist: args = args + ['--'] + filelist
+        """Returns an array of dicts containing revision info for refspec"""
+        args = [
+            '--no-pager', 'log',
+            '--pretty={'
+            '%x00short%x00:%x00%h%x00,'
+            '%x00revid%x00:%x00%H%x00,'
+            '%x00author%x00:%x00%an <%ae>%x00,'
+            '%x00date%x00:%ct,'
+            '%x00summary%x00:%x00%s%x00'
+            '}'
+        ]
+        if refspec:
+            args += ['-1', refspec]
+        elif maxres:
+            args += ['-{0:d}'.format(maxres)]
+        if filelist:
+            args += ['--'] + filelist
 
-        raw = self._git(self.path, args, catchout=True)
-        L = re.findall('^\s*(\w*)\s*(\w*)\s*^Author:\s*(.*)\s*^Date:\s*(.*)\s*^Subject:\s*(.*)\s*', raw, re.MULTILINE)
+        try:
+            output = self._run(args).rstrip('\n')
+        except VcsError:
+            return None
+        if not output:
+            return None
 
         log = []
-        for t in L:
-            dt = {}
-            dt['short'] = t[0].strip()
-            dt['revid'] = t[1].strip()
-            dt['author'] = t[2].strip()
-            m = re.match('\d+(\.\d+)?', t[3].strip())
-            dt['date'] = datetime.fromtimestamp(float(m.group(0)))
-            dt['summary'] = t[4].strip()
-            log.append(dt)
+        for line in output\
+                .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'):
+            line = json.loads(line)
+            line['date'] = datetime.fromtimestamp(line['date'])
+            log.append(line)
         return log
 
-
-    def _git_file_status(self, st):
-        if len(st) != 2: raise VcsError("Wrong git file status string: %s" % st)
-        X, Y = (st[0], st[1])
-        if   X in " "      and Y in " " : return 'sync'
-        elif X in "MADRC"  and Y in " " : return 'staged'
-        elif X in "MADRC " and Y in "M":  return 'changed'
-        elif X in "MARC "  and Y in "D":  return 'deleted'
-        elif X in "U" or Y in "U":        return 'conflict'
-        elif X in "A" and Y in "A":       return 'conflict'
-        elif X in "D" and Y in "D":       return 'conflict'
-        elif X in "?" and Y in "?":       return 'untracked'
-        elif X in "!" and Y in "!":       return 'ignored'
-        else:                             return 'unknown'
-
-
-
-    # Repo creation
-    #---------------------------
-
-    def init(self):
-        """Initializes a repo in current path"""
-        self._git(self.path, ['init'])
-        self.update()
-
-
-    def clone(self, src):
-        """Clones a repo from src"""
-        name = os.path.basename(self.path)
-        path = os.path.dirname(self.path)
-        try:
-            os.rmdir(self.path)
-        except OSError:
-            raise VcsError("Can't clone to %s. It is not an empty directory" % self.path)
-
-        self._git(path, ['clone', src, name])
-        self.update()
-
-
+    def _status_translate(self, code):
+        """Translate status code"""
+        for code_x, code_y, status in self._status_translations:
+            if code[0] in code_x and code[1] in code_y:
+                return status
+        return 'unknown'
 
     # Action interface
-    #---------------------------
-
-    def commit(self, message):
-        """Commits with a given message"""
-        self._git(self.path, ['commit', '-m', message])
-
-
-    def add(self, filelist=None):
-        """Adds files to the index, preparing for commit"""
-        if filelist != None: self._git(self.path, ['add', '-A'] + filelist)
-        else:                self._git(self.path, ['add', '-A'])
-
-
-    def reset(self, filelist=None):
-        """Removes files from the index"""
-        if filelist != None: self._git(self.path, ['reset'] + filelist)
-        else:                self._git(self.path, ['reset'])
-
-
-    def pull(self, br=None):
-        """Pulls from remote"""
-        if br: self._git(self.path, ['pull', br])
-        else:  self._git(self.path, ['pull'])
-
-
-    def push(self, br=None):
-        """Pushes to remote"""
-        if br: self._git(self.path, ['push', br])
-        else:  self._git(self.path, ['push'])
-
-
-    def checkout(self, rev):
-        """Checks out a branch or revision"""
-        self._git(self.path, ['checkout', self._sanitize_rev(rev)])
-
-
-    def extract_file(self, rev, name, dest):
-        """Extracts a file from a given revision and stores it in dest dir"""
-        if rev == self.INDEX:
-            shutil.copyfile(os.path.join(self.path, name), dest)
-        else:
-            out = self._git(self.path, ['--no-pager', 'show', '%s:%s' % (self._sanitize_rev(rev), name)],
-                            catchout=True, bytes=True)
-            with open(dest, 'wb') as fd: fd.write(out)
 
+    def action_add(self, filelist=None):
+        args = ['add', '--all']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
 
+    def action_reset(self, filelist=None):
+        args = ['reset']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
 
     # Data Interface
-    #---------------------------
-
-    def get_status_allfiles(self):
-        """Returns a dict indexed by files not in sync their status as values.
-           Paths are given relative to the root. Strips trailing '/' from dirs."""
-        raw = self._git(self.path, ['status', '--porcelain'], catchout=True, bytes=True)
-        L = re.findall('^(..)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        ret = {}
-        for st, p in L:
-            sta = self._git_file_status(st)
-            if 'R' in st:
-                m = re.match('^(.*)\->(.*)$', p)
-                if m: p = m.group(2).strip()
-            ret[os.path.normpath(p.strip())] = sta
-        return ret
-
-
-    def get_ignore_allfiles(self):
-        """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs."""
-        raw = self._git(self.path, ['ls-files', '--others', '--directory', '-i', '--exclude-standard'],
-                        catchout=True)
-        return set(os.path.normpath(p) for p in raw.split('\n'))
-
-
-    def get_remote_status(self):
-        """Checks the status of the repo regarding sync state with remote branch"""
+
+    def data_status_root(self):
+        statuses = set()
+
+        # Paths with status
+        skip = False
+        output = self._run(['status', '--porcelain', '-z']).rstrip('\x00')
+        if not output:
+            return 'sync'
+        for line in output.split('\x00'):
+            if skip:
+                skip = False
+                continue
+            statuses.add(self._status_translate(line[:2]))
+            if line.startswith('R'):
+                skip = True
+
+        for status in self.DIRSTATUSES:
+            if status in statuses:
+                return status
+        return 'sync'
+
+    def data_status_subpaths(self):
+        statuses = {}
+
+        # Ignored directories
+        output = self._run([
+            'ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'
+        ]).rstrip('\x00')
+        if output:
+            for path in output.split('\x00'):
+                if path.endswith('/'):
+                    statuses[os.path.normpath(path)] = 'ignored'
+
+        # Empty directories
+        output = self._run(
+            ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).rstrip('\x00')
+        if output:
+            for path in output.split('\x00'):
+                if path.endswith('/'):
+                    statuses[os.path.normpath(path)] = 'none'
+
+        # Paths with status
+        output = self._run(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00')
+        if output:
+            skip = False
+            for line in output.split('\x00'):
+                if skip:
+                    skip = False
+                    continue
+                statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2])
+                if line.startswith('R'):
+                    skip = True
+
+        return statuses
+
+    def data_status_remote(self):
         try:
             head = self._head_ref()
             remote = self._remote_ref(head)
         except VcsError:
             head = remote = None
+        if not head or not remote:
+            return 'none'
+
+        output = self._run(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)])
+        ahead = re.search(r'^>', output, flags=re.MULTILINE)
+        behind = re.search(r'^<', output, flags=re.MULTILINE)
+        if ahead:
+            return 'diverged' if behind else 'ahead'
+        else:
+            return 'behind' if behind else 'sync'
 
-        if head and remote:
-            raw = self._git(self.path, ['rev-list', '--left-right', '%s...%s' % (remote, head)], catchout=True)
-            ahead  = re.search("^>", raw, flags=re.MULTILINE)
-            behind = re.search("^<", raw, flags=re.MULTILINE)
-
-            if       ahead and     behind: return "diverged"
-            elif     ahead and not behind: return "ahead"
-            elif not ahead and     behind: return "behind"
-            elif not ahead and not behind: return "sync"
-        else:                            return "none"
-
-
-    def get_branch(self):
-        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
+    def data_branch(self):
         try:
             head = self._head_ref()
         except VcsError:
             head = None
-
-        if head:
-            m = re.match('refs/heads/([^/]*)', head)
-            if m: return m.group(1).strip()
-        else:
-            return "detached"
-
-        return None
-
-
-    def get_log(self, filelist=None, maxres=None):
-        """Get the entire log for the current HEAD"""
-        if not self._has_head(): return []
-        return self._log(refspec=None, maxres=maxres, filelist=filelist)
-
-
-    def get_raw_log(self, filelist=None):
-        """Gets the raw log as a string"""
-        if not self._has_head(): return []
-        args = ['log']
-        if filelist: args = args + ['--'] + filelist
-        return self._git(self.path, args, catchout=True)
-
-
-    def get_raw_diff(self, refspec=None, filelist=None):
-        """Gets the raw diff as a string"""
-        args = ['diff']
-        if refspec:  args = args + [refspec]
-        if filelist: args = args + ['--'] + filelist
-        return self._git(self.path, args, catchout=True)
-
-
-    def get_remote(self):
-        """Returns the url for the remote repo attached to head"""
-        if self.is_repo():
-            try:
-                ref = self._head_ref()
-                remote = self._remote_ref(ref)
-            except VcsError:
-                ref = remote = None
-
-            if remote:
-                m = re.match('refs/remotes/([^/]*)/', remote)
-                if m:
-                    url = self._git(self.path, ['config', '--get', 'remote.%s.url' % m.group(1)], catchout=True)
-                    return url.strip() or None
-        return None
-
-
-    def get_revision_id(self, rev=None):
-        """Get a canonical key for the revision rev"""
-        if rev == None: rev = self.HEAD
-        elif rev == self.INDEX: return None
-        rev = self._sanitize_rev(rev)
-
-        return self._sanitize_rev(self._git(self.path, ['rev-parse', rev], catchout=True))
-
-
-    def get_info(self, rev=None):
-        """Gets info about the given revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-        if rev == self.HEAD and not self._has_head(): return None
-
-        L = self._log(refspec=rev)
-        if len(L) == 0:
-            raise VcsError("Revision %s does not exist" % rev)
-        elif len(L) > 1:
-            raise VcsError("More than one instance of revision %s ?!?" % rev)
-        else:
-            return L[0]
-
-
-    def get_files(self, rev=None):
-        """Gets a list of files in revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-
-        if rev:
-            if rev == self.INDEX:  raw = self._git(self.path, ["ls-files"], catchout=True)
-            else:                  raw = self._git(self.path, ['ls-tree', '--name-only', '-r', rev], catchout=True)
-            return raw.split('\n')
+        if head is None:
+            return 'detached'
+
+        match = re.match('refs/heads/([^/]+)', head)
+        return match.group(1) if match else None
+
+    def data_info(self, rev=None):
+        if rev is None:
+            rev = self.HEAD
+
+        log = self._log(refspec=rev)
+        if not log:
+            if rev == self.HEAD:
+                return None
+            else:
+                raise VcsError('Revision {0:s} does not exist'.format(rev))
+        elif len(log) == 1:
+            return log[0]
         else:
-            return []
-
-# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
+            raise VcsError('More than one instance of revision {0:s}'.format(rev))
diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py
index b8731dbf..cf15e35e 100644
--- a/ranger/ext/vcs/hg.py
+++ b/ranger/ext/vcs/hg.py
@@ -1,271 +1,135 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011-2012
-#
-# vcs - a python module to handle various version control systems
 
-import os
-import re
-import shutil
+"""Mercurial module"""
+
 from datetime import datetime
-try:
-    from configparser import RawConfigParser
-except ImportError:
-    from ConfigParser import RawConfigParser
+import json
+import os
 
 from .vcs import Vcs, VcsError
 
 
 class Hg(Vcs):
-    vcsname  = 'hg'
+    """VCS implementation for Mercurial"""
     HEAD = 'tip'
-    # Auxiliar stuff
-    #---------------------------
-
-    def _hg(self, path, args, silent=True, catchout=False, bytes=False):
-        return self._vcs(path, 'hg', args, silent=silent, catchout=catchout, bytes=bytes)
-
 
-    def _has_head(self):
-        """Checks whether repo has head"""
-        rnum = self._hg(self.path, ['-q', 'identify', '--num', '-r', self.HEAD], catchout=True)
-        return rnum != '-1'
-
-
-    def _sanitize_rev(self, rev):
-        if rev == None: return None
-        rev = rev.strip()
-        if len(rev) == 0: return None
-        if rev[-1] == '+': rev = rev[:-1]
-
-        try:
-            if int(rev) == 0: return None
-        except:
-            pass
+    _status_translations = (
+        ('AR', 'staged'),
+        ('M', 'changed'),
+        ('!', 'deleted'),
 
-        return rev
+        ('?', 'untracked'),
+        ('I', 'ignored'),
+    )
 
+    # Generic
 
     def _log(self, refspec=None, maxres=None, filelist=None):
 
-        fmt = "changeset: {rev}:{node}\ntag: {tags}\nuser: {author}\ndate: {date}\nsummary: {desc}\n"
-        args = ['log', '--template', fmt]
+        args = ['log', '--template', 'json']
+        if refspec:
+            args += ['--limit', '1', '--rev', refspec]
+        elif maxres:
+            args += ['--limit', str(maxres)]
+        if filelist:
+            args += ['--'] + filelist
 
-        if refspec:  args = args + ['--limit', '1', '-r', refspec]
-        elif maxres: args = args + ['--limit', str(maxres)]
-
-        if filelist: args = args + filelist
-
-        raw = self._hg(self.path, args, catchout=True)
-        L = re.findall('^changeset:\s*([0-9]*):([0-9a-zA-Z]*)\s*$\s*^tag:\s*(.*)\s*$\s*^user:\s*(.*)\s*$\s*^date:\s*(.*)$\s*^summary:\s*(.*)\s*$', raw, re.MULTILINE)
+        try:
+            output = self._run(args).rstrip('\n')
+        except VcsError:
+            return None
+        if not output:
+            return None
 
         log = []
-        for t in L:
-            dt = {}
-            dt['short'] = t[0].strip()
-            dt['revid'] = self._sanitize_rev(t[1].strip())
-            dt['author'] = t[3].strip()
-            m = re.match('\d+(\.\d+)?', t[4].strip())
-            dt['date'] = datetime.fromtimestamp(float(m.group(0)))
-            dt['summary'] = t[5].strip()
-            log.append(dt)
+        for entry in json.loads(output):
+            new = {}
+            new['short'] = entry['rev']
+            new['revid'] = entry['node']
+            new['author'] = entry['user']
+            new['date'] = datetime.fromtimestamp(entry['date'][0])
+            new['summary'] = entry['desc']
+            log.append(new)
         return log
 
-
-    def _hg_file_status(self, st):
-        if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st)
-        if   st in "ARM":    return 'staged'
-        elif st in "!":      return 'deleted'
-        elif st in "I":      return 'ignored'
-        elif st in "?":      return 'untracked'
-        elif st in "X":      return 'conflict'
-        elif st in "C":      return 'sync'
-        else:                return 'unknown'
-
-
-
-    # Repo creation
-    #---------------------------
-
-    def init(self):
-        """Initializes a repo in current path"""
-        self._hg(self.path, ['init'])
-        self.update()
-
-
-    def clone(self, src):
-        """Clones a repo from src"""
-        name = os.path.basename(self.path)
-        path = os.path.dirname(self.path)
-        try:
-            os.rmdir(self.path)
-        except OSError:
-            raise VcsError("Can't clone to %s. It is not an empty directory" % self.path)
-
-        self._hg(path, ['clone', src, name])
-        self.update()
-
-
-
-    # Action Interface
-    #---------------------------
-
-    def commit(self, message):
-        """Commits with a given message"""
-        self._hg(self.path, ['commit', '-m', message])
-
-
-    def add(self, filelist=None):
-        """Adds files to the index, preparing for commit"""
-        if filelist != None: self._hg(self.path, ['addremove'] + filelist)
-        else:                self._hg(self.path, ['addremove'])
-
-
-    def reset(self, filelist=None):
-        """Removes files from the index"""
-        if filelist == None: filelist = self.get_status_allfiles().keys()
-        self._hg(self.path, ['forget'] + filelist)
-
-
-    def pull(self):
-        """Pulls a hg repo"""
-        self._hg(self.path, ['pull', '-u'])
-
-
-    def push(self):
-        """Pushes a hg repo"""
-        self._hg(self.path, ['push'])
-
-
-    def checkout(self, rev):
-        """Checks out a branch or revision"""
-        self._hg(self.path, ['update', rev])
-
-
-    def extract_file(self, rev, name, dest):
-        """Extracts a file from a given revision and stores it in dest dir"""
-        if rev == self.INDEX:
-            shutil.copyfile(os.path.join(self.path, name), dest)
-        else:
-            self._hg(self.path, ['cat', '--rev', rev, '--output', dest, name])
-
-
-    # Data Interface
-    #---------------------------
-
-    def get_status_allfiles(self):
-        """Returns a dict indexed by files not in sync their status as values.
-           Paths are given relative to the root. Strips trailing '/' from dirs."""
-        raw = self._hg(self.path, ['status'], catchout=True, bytes=True)
-        L = re.findall('^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        ret = {}
-        for st, p in L:
-            # Detect conflict by the existence of .orig files
-            if st == '?' and re.match('^.*\.orig\s*$', p):  st = 'X'
-            sta = self._hg_file_status(st)
-            ret[os.path.normpath(p.strip())] = sta
-        return ret
-
-
-    def get_ignore_allfiles(self):
-        """Returns a set of all the ignored files in the repo"""
-        raw = self._hg(self.path, ['status', '-i'], catchout=True, bytes=True)
-        L = re.findall('^I\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        return set(L)
-
-
-    def get_remote_status(self):
-        """Checks the status of the repo regarding sync state with remote branch"""
-        if self.get_remote() == None:
-            return "none"
-
-        ahead = behind = True
+    def _remote_url(self):
+        """Remote url"""
         try:
-            self._hg(self.path, ['outgoing'], silent=True)
-        except:
-            ahead = False
-
-        try:
-            self._hg(self.path, ['incoming'], silent=True)
-        except:
-            behind = False
-
-        if       ahead and     behind: return "diverged"
-        elif     ahead and not behind: return "ahead"
-        elif not ahead and     behind: return "behind"
-        elif not ahead and not behind: return "sync"
-
-
-    def get_branch(self):
-        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
-        branch = self._hg(self.path, ['branch'], catchout=True)
-        return branch or None
-
-
-    def get_log(self, filelist=None, maxres=None):
-        """Get the entire log for the current HEAD"""
-        if not self._has_head(): return []
-        return self._log(refspec=None, maxres=maxres, filelist=filelist)
-
-
-    def get_raw_log(self, filelist=None):
-        """Gets the raw log as a string"""
-        if not self._has_head(): return []
-        args = ['log']
-        if filelist: args = args + filelist
-        return self._hg(self.path, args, catchout=True)
-
-
-    def get_raw_diff(self, refspec=None, filelist=None):
-        """Gets the raw diff as a string"""
-        args = ['diff', '--git']
-        if refspec:  args = args + [refspec]
-        if filelist: args = args + filelist
-        return self._hg(self.path, args, catchout=True)
-
-
-    def get_remote(self, rev=None):
-        """Returns the url for the remote repo attached to head"""
-        remote = self._hg(self.path, ['showconfig', 'paths.default'], catchout=True)
-        return remote or None
-
-
-    def get_revision_id(self, rev=None):
-        """Get a canonical key for the revision rev"""
-        if rev == None: rev = self.HEAD
-        elif rev == self.INDEX: return None
-        rev = self._sanitize_rev(rev)
-
-        return self._sanitize_rev(self._hg(self.path, ['-q', 'identify', '--id', '-r', rev], catchout=True))
-
-
-    def get_info(self, rev=None):
-        """Gets info about the given revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-        if rev == self.HEAD and not self._has_head(): return None
-
-        L = self._log(refspec=rev)
-        if len(L) == 0:
-            raise VcsError("Revision %s does not exist" % rev)
-        elif len(L) > 1:
-            raise VcsError("More than one instance of revision %s ?!?" % rev)
+            return self._run(['showconfig', 'paths.default']).rstrip('\n') or None
+        except VcsError:
+            return None
+
+    def _status_translate(self, code):
+        """Translate status code"""
+        for code_x, status in self._status_translations:  # pylint: disable=invalid-name
+            if code in code_x:
+                return status
+        return 'unknown'
+
+    # Action interface
+
+    def action_add(self, filelist=None):
+        args = ['add']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
+
+    def action_reset(self, filelist=None):
+        args = ['forget', '--']
+        if filelist:
+            args += filelist
         else:
-            return L[0]
-
-
-    def get_files(self, rev=None):
-        """Gets a list of files in revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-
-        if rev:
-            if rev == self.INDEX: raw = self._hg(self.path, ['locate', "*"], catchout=True)
-            else:                 raw = self._hg(self.path, ['locate', '--rev', rev, "*"], catchout=True)
-            return raw.split('\n')
+            args += self.rootvcs.status_subpaths.keys()
+        self._run(args, catchout=False)
+
+    # Data interface
+
+    def data_status_root(self):
+        statuses = set()
+
+        # Paths with status
+        for entry in json.loads(self._run(['status', '--all', '--template', 'json'])):
+            if entry['status'] == 'C':
+                continue
+            statuses.add(self._status_translate(entry['status']))
+
+        if statuses:
+            for status in self.DIRSTATUSES:
+                if status in statuses:
+                    return status
+        return 'sync'
+
+    def data_status_subpaths(self):
+        statuses = {}
+
+        # Paths with status
+        for entry in json.loads(self._run(['status', '--all', '--template', 'json'])):
+            if entry['status'] == 'C':
+                continue
+            statuses[os.path.normpath(entry['path'])] = self._status_translate(entry['status'])
+
+        return statuses
+
+    def data_status_remote(self):
+        if self._remote_url() is None:
+            return 'none'
+        return 'unknown'
+
+    def data_branch(self):
+        return self._run(['branch']).rstrip('\n') or None
+
+    def data_info(self, rev=None):
+        if rev is None:
+            rev = self.HEAD
+
+        log = self._log(refspec=rev)
+        if not log:
+            if rev == self.HEAD:
+                return None
+            else:
+                raise VcsError('Revision {0:s} does not exist'.format(rev))
+        elif len(log) == 1:
+            return log[0]
         else:
-            return []
-
-
-# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
+            raise VcsError('More than one instance of revision {0:s}'.format(rev))
diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py
index 9bf8698c..1813f857 100644
--- a/ranger/ext/vcs/svn.py
+++ b/ranger/ext/vcs/svn.py
@@ -1,274 +1,148 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011-2012
-#         Ryan Burns <rdburns@gmail.com>, 2015
-#
-# R. Burns start with Abdó's Hg module and modified it for Subversion.
-#
-# vcs - a python module to handle various version control systems
 
-from __future__ import with_statement
-import os
-import re
-import shutil
-import logging
+"""Subversion module"""
+
 from datetime import datetime
+import os
+from xml.etree import ElementTree as etree
+
 from .vcs import Vcs, VcsError
 
-#logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG,
-#                    filemode='w')
 
 class SVN(Vcs):
-    vcsname = 'svn'
-    HEAD = 'HEAD'
-    # Auxiliar stuff
-    #---------------------------
-
-    def _svn(self, path, args, silent=True, catchout=False, bytes=False):
-        return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, bytes=bytes)
-
-
-    def _has_head(self):
-        """Checks whether repo has head"""
-        return True
-
-
-    def _sanitize_rev(self, rev):
-        if rev == None: return None
-        rev = rev.strip()
-        if len(rev) == 0: return None
-        if rev[-1] == '+': rev = rev[:-1]
-
-        try:
-            if int(rev) == 0: return None
-        except:
-            pass
-
-        return rev
-
+    """VCS implementation for Subversion"""
+    # Generic
+    _status_translations = (
+        ('ADR', 'staged'),
+        ('C', 'conflict'),
+        ('I', 'ignored'),
+        ('M~', 'changed'),
+        ('X', 'none'),
+        ('?', 'untracked'),
+        ('!', 'deleted'),
+    )
 
     def _log(self, refspec=None, maxres=None, filelist=None):
-        """ Retrieves log message and parses it.
-        """
-        args = ['log']
+        """Retrieves log message and parses it"""
+        args = ['log', '--xml']
 
-        if refspec:  args = args + ['--limit', '1', '-r', refspec]
-        elif maxres: args = args + ['--limit', str(maxres)]
+        if refspec:
+            args += ['--limit', '1', '--revision', refspec]
+        elif maxres:
+            args += ['--limit', str(maxres)]
 
-        if filelist: args = args + filelist
-        logging.debug('Running svn log')
-        logging.debug(args)
+        if filelist:
+            args += ['--'] + filelist
 
-        raw = self._svn(self.path, args, catchout=True)
-        logging.debug(raw)
-        L = re.findall(r"""^[-]*\s*            # Dash line
-                       r([0-9]*)\s\|\s         # Revision          [0]
-                       (.*)\s\|\s              # Author            [1]
-                       (.*?)\s                 # Date              [2]
-                       (.*?)\s                 # Time              [3]
-                       .*?\|\s*?               # Dump rest of date string
-                       ([0-9])+\sline(?:s)?\s* # Number of line(s) [4]
-                       (.*)\s                  # Log Message       [5]
-                       [-]+\s*                 # Dash line
-                       $""", raw, re.MULTILINE | re.VERBOSE)
+        try:
+            output = self._run(args).rstrip('\n')
+        except VcsError:
+            return None
+        if not output:
+            return None
 
         log = []
-        for t in L:
-            logging.debug(t)
-            dt = {}
-            dt['short'] = t[0].strip()
-            dt['revid'] = t[0].strip()
-            dt['author'] = t[1].strip()
-            dt['date'] = datetime.strptime(t[2]+'T'+t[3], "%Y-%m-%dT%H:%M:%S")
-            dt['summary'] = t[5].strip()
-            log.append(dt)
-            logging.debug(log)
+        for entry in etree.fromstring(output).findall('./logentry'):
+            new = {}
+            new['short'] = entry.get('revision')
+            new['revid'] = entry.get('revision')
+            new['author'] = entry.find('./author').text
+            new['date'] = datetime.strptime(
+                entry.find('./date').text,
+                '%Y-%m-%dT%H:%M:%S.%fZ',
+            )
+            new['summary'] = entry.find('./msg').text.split('\n')[0]
+            log.append(new)
         return log
 
+    def _status_translate(self, code):
+        """Translate status code"""
+        for code_x, status in self._status_translations:
+            if code in code_x:
+                return status
+        return 'unknown'
 
-    def _svn_file_status(self, st):
-        if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st)
-        if   st in "A":  return 'staged'
-        elif st in "D":  return 'deleted'
-        elif st in "I":  return 'ignored'
-        elif st in "?":  return 'untracked'
-        elif st in "C":  return 'conflict'
-        else:            return 'unknown'
-
-
-
-    # Repo creation
-    #---------------------------
-
-    def init(self):
-        """Initializes a repo in current path"""
-        self._svn(self.path, ['init'])
-        self.update()
-
-
-    def clone(self, src):
-        """Checks out SVN repo"""
-        name = os.path.basename(self.path)
-        path = os.path.dirname(self.path)
+    def _remote_url(self):
+        """Remote url"""
         try:
-            os.rmdir(self.path)
-        except OSError:
-            raise VcsError("Can't clone to %s. It is not an empty directory" % self.path)
-
-        self._svn(path, ['co', src, name])
-        self.update()
-
-
+            output = self._run(['info', '--xml']).rstrip('\n')
+        except VcsError:
+            return None
+        if not output:
+            return None
+        return etree.fromstring(output).find('./entry/url').text or None
 
     # Action Interface
-    #---------------------------
-
-    def commit(self, message):
-        """Commits with a given message"""
-        self._svn(self.path, ['commit', '-m', message])
-
-
-    def add(self, filelist=None):
-        """Adds files to the index, preparing for commit"""
-        if filelist != None: self._svn(self.path, ['add'] + filelist)
-        else:                self._svn(self.path, ['add'])
-
-
-    def reset(self, filelist=None):
-        """Equivalent to svn revert"""
-        if filelist == None: filelist = self.get_status_allfiles().keys()
-        self._svn(self.path, ['revert'] + filelist)
 
+    def action_add(self, filelist=None):
+        args = ['add']
+        if filelist:
+            args += ['--'] + filelist
+        self._run(args, catchout=False)
 
-    def pull(self):
-        """Executes SVN Update"""
-        self._svn(self.path, ['update'])
-
-
-    def push(self):
-        """Push doesn't have an SVN analog."""
-        raise NotImplementedError
-
-
-    def checkout(self, rev):
-        """Checks out a branch or revision"""
-        raise NotImplementedError
-        self._svn(self.path, ['update', rev])
-
-
-    def extract_file(self, rev, name, dest):
-        """Extracts a file from a given revision and stores it in dest dir"""
-        if rev == self.INDEX:
-            shutil.copyfile(os.path.join(self.path, name), dest)
+    def action_reset(self, filelist=None):
+        args = ['revert', '--']
+        if filelist:
+            args += filelist
         else:
-            file_contents = self._svn(self.path, ['cat', '-r', rev, name], catchout=True)
-            with open(dest, 'w') as f:
-                f.write(file_contents)
-
+            args += self.rootvcs.status_subpaths.keys()
+        self._run(args, catchout=False)
 
     # Data Interface
-    #---------------------------
 
-    def get_status_allfiles(self):
-        """Returns a dict indexed by files not in sync their status as values.
-           Paths are given relative to the root. Strips trailing '/' from dirs."""
-        raw = self._svn(self.path, ['status'], catchout=True, bytes=True)
-#        logging.debug(raw)
-        L = re.findall(r'^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        ret = {}
-        for st, p in L:
-            # Detect conflict by the existence of .orig files
-            if st == '?' and re.match(r'^.*\.orig\s*$', p): st = 'X'
-            sta = self._svn_file_status(st)
-            ret[os.path.normpath(p.strip())] = sta
-        return ret
-
-
-    def get_ignore_allfiles(self):
-        """Returns a set of all the ignored files in the repo"""
-        raw = self._svn(self.path, ['status'], catchout=True, bytes=True)
-#        logging.debug(raw)
-        L = re.findall(r'^I\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
-        return set(L)
-
-
-    def get_remote_status(self):
-        """Checks the status of the repo regarding sync state with remote branch.
-
-        I'm not sure this make sense for SVN so we're just going to return 'sync'"""
+    def data_status_root(self):
+        statuses = set()
+
+        # Paths with status
+        output = self._run(['status']).rstrip('\n')
+        if not output:
+            return 'sync'
+        for line in output.split('\n'):
+            code = line[0]
+            if code == ' ':
+                continue
+            statuses.add(self._status_translate(code))
+
+        for status in self.DIRSTATUSES:
+            if status in statuses:
+                return status
         return 'sync'
 
-    def get_branch(self):
-        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
-        return None
-        branch = self._svn(self.path, ['branch'], catchout=True)
-        return branch or None
-
+    def data_status_subpaths(self):
+        statuses = {}
 
-    def get_log(self, filelist=None, maxres=None):
-        """Get the entire log for the current HEAD"""
-        if not self._has_head(): return []
-        return self._log(refspec=None, maxres=maxres, filelist=filelist)
+        # Paths with status
+        output = self._run(['status']).rstrip('\n')
+        if output:
+            for line in output.split('\n'):
+                code, path = line[0], line[8:]
+                if code == ' ':
+                    continue
+                statuses[os.path.normpath(path)] = self._status_translate(code)
 
+        return statuses
 
-    def get_raw_log(self, filelist=None):
-        """Gets the raw log as a string"""
-        if not self._has_head(): return []
-        args = ['log']
-        if filelist: args = args + filelist
-        return self._svn(self.path, args, catchout=True)
-
-
-    def get_raw_diff(self, refspec=None, filelist=None):
-        """Gets the raw diff as a string"""
-        args = ['diff', '--git']
-        if refspec:  args = args + [refspec]
-        if filelist: args = args + filelist
-        return self._svn(self.path, args, catchout=True)
-
-
-    def get_remote(self, rev=None):
-        """Returns the url for the remote repo attached to head"""
-        raw = self.get_info(rev=rev)
-        L = re.findall('URL: (.*)\n', raw.decode('utf-8'))
-
-        return L[0][0]
-
-
-    def get_revision_id(self, rev=None):
-        """Get a canonical key for the revision rev.
-
-        This is just returning the rev for SVN"""
-        if rev == None: rev = self.HEAD
-        elif rev == self.INDEX: return None
-        rev = self._sanitize_rev(rev)
-        return rev
+    def data_status_remote(self):
+        remote = self._remote_url()
+        if remote is None or remote.startswith('file://'):
+            return 'none'
+        return 'unknown'
 
+    def data_branch(self):
+        return None
 
-    def get_info(self, rev=None):
-        """Gets info about the given revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-        if rev == self.HEAD and not self._has_head(): return None
-        logging.debug('refspec is ' + str(rev))
-        L = self._log(refspec=rev)
-        logging.debug(len(L))
-        if len(L) == 0:
-            raise VcsError("Revision %s does not exist" % rev)
-        elif len(L) > 1:
-            raise VcsError("More than one instance of revision %s ?!?" % rev)
+    def data_info(self, rev=None):
+        if rev is None:
+            rev = self.HEAD
+
+        log = self._log(refspec=rev)
+        if not log:
+            if rev == self.HEAD:
+                return None
+            else:
+                raise VcsError('Revision {0:s} does not exist'.format(rev))
+        elif len(log) == 1:
+            return log[0]
         else:
-            return L[0]
-
-
-    def get_files(self, rev=None):
-        """Gets a list of files in revision rev"""
-        if rev == None: rev = self.HEAD
-        rev = self._sanitize_rev(rev)
-
-        raw = self._svn(self.path, ['ls', "-r", rev], catchout=True)
-        return raw.split('\n')
-
-
-# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
+            raise VcsError('More than one instance of revision {0:s}'.format(rev))
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index 3f7236f5..fd34c3fe 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -1,348 +1,488 @@
-# -*- coding: utf-8 -*-
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
-# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011-2012
-#
-# vcs - a python module to handle various version control systems
+
+"""VCS module"""
 
 import os
 import subprocess
-from datetime import datetime
+import threading
+import time
+
+# Python2 compatibility
+try:
+    import queue
+except ImportError:
+    import Queue as queue  # pylint: disable=import-error
+try:
+    FileNotFoundError
+except NameError:
+    FileNotFoundError = OSError  # pylint: disable=redefined-builtin
 
 
 class VcsError(Exception):
+    """VCS exception"""
     pass
 
 
-class Vcs(object):
-    """ This class represents a version controlled path, abstracting the usual
-        operations from the different supported backends.
-
-        The backends are declared in te variable self.repo_types, and are derived
-        classes from Vcs with the following restrictions:
+class Vcs(object):  # pylint: disable=too-many-instance-attributes
+    """
+    This class represents a version controlled path, abstracting the usual
+    operations from the different supported backends.
 
-         * do NOT implement __init__. Vcs takes care of this.
+    The backends are declared in REPOTYPES, and are derived
+    classes from Vcs with the following restrictions:
 
-         * do not create change internal state. All internal state should be
-           handled in Vcs
+     * Override ALL interface methods
+     * Only override interface methods
+     * Do NOT modify internal state. All internal state is handled by Vcs
 
-        Objects from backend classes should never be created directly. Instead
-        create objects of Vcs class. The initialization calls update, which takes
-        care of detecting the right Vcs backend to use and dynamically changes the
-        object type accordingly.
-        """
+    """
 
-    # These are abstracted revs, representing the current index (staged files),
+    # These are abstracted revisions, representing the current index (staged files),
     # the current head and nothing. Every backend should redefine them if the
     # version control has a similar concept, or implement _sanitize_rev method to
     # clean the rev before using them
-    INDEX    = "INDEX"
-    HEAD     = "HEAD"
-    NONE     = "NONE"
-    vcsname  = None
-
-    # Possible status responses
-    FILE_STATUS   = ['conflict', 'untracked', 'deleted', 'changed', 'staged', 'ignored', 'sync', 'none', 'unknown']
-    REMOTE_STATUS = ['none', 'sync', 'behind', 'ahead', 'diverged', 'unknown']
-
-
-    def __init__(self, path, vcstype=None):
-        # This is a bit hackish, but I need to import here to avoid circular imports
-        from .git import Git
-        from .hg  import Hg
-        from .bzr import Bzr
-        from .svn import SVN
-        self.repo_types  = {'git': Git, 'hg': Hg, 'bzr': Bzr, 'svn': SVN}
-
-        self.path = os.path.expanduser(path)
-        self.status = {}
-        self.ignored = set()
-        self.root = None
-
-        self.update(vcstype=vcstype)
-
-
-    # Auxiliar
-    #---------------------------
-
-    def _vcs(self, path, cmd, args, silent=False, catchout=False, bytes=False):
-        """Executes a vcs command"""
-        with open('/dev/null', 'w') as devnull:
-            if silent: out=devnull
-            else:      out=None
+    INDEX = 'INDEX'
+    HEAD = 'HEAD'
+    NONE = 'NONE'
+
+    # Backends
+    REPOTYPES = {
+        'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr'},
+        'git': {'class': 'Git', 'setting': 'vcs_backend_git'},
+        'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg'},
+        'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn'},
+    }
+
+    # Possible directory statuses in order of importance
+    # statuses that should not be inherited from subpaths are disabled
+    DIRSTATUSES = (
+        'conflict',
+        'untracked',
+        'deleted',
+        'changed',
+        'staged',
+        # 'ignored',
+        'sync',
+        # 'none',
+        'unknown',
+    )
+
+    def __init__(self, dirobj):
+        self.obj = dirobj
+        self.path = dirobj.path
+        self.repotypes_settings = set(
+            repotype for repotype, values in self.REPOTYPES.items()
+            if getattr(dirobj.settings, values['setting']) in ('enabled', 'local')
+        )
+
+        self.root, self.repodir, self.repotype, self.links = self._find_root(self.path)
+        self.is_root = True if self.obj.path == self.root else False
+        self.is_root_link = True if self.obj.is_link and self.obj.realpath == self.root else False
+        self.is_root_pointer = self.is_root or self.is_root_link
+        self.in_repodir = False
+        self.rootvcs = None
+        self.track = False
+
+        if self.root:
+            if self.is_root:
+                self.rootvcs = self
+                self.__class__ = globals()[self.REPOTYPES[self.repotype]['class'] + 'Root']
+
+                if not os.access(self.repodir, os.R_OK):
+                    self.obj.vcsremotestatus = 'unknown'
+                    self.obj.vcsstatus = 'unknown'
+                    return
+
+                self.track = True
+            else:
+                self.rootvcs = dirobj.fm.get_directory(self.root).vcs
+                if self.rootvcs is None or self.rootvcs.root is None:
+                    return
+                self.rootvcs.links |= self.links
+                self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']]
+                self.track = self.rootvcs.track
+
+                if self.path == self.repodir or self.path.startswith(self.repodir + '/'):
+                    self.in_repodir = True
+                    self.track = False
+
+    # Generic
+
+    def _run(self, args, path=None, catchout=True, retbytes=False):
+        """Run a command"""
+        cmd = [self.repotype] + args
+        if path is None:
+            path = self.path
+
+        with open(os.devnull, 'w') as devnull:
             try:
                 if catchout:
-                    raw = subprocess.check_output([cmd] + args, stderr=out, cwd=path)
-                    if bytes: return raw
-                    else:     return raw.decode('utf-8', errors="ignore").strip()
-                else:
-                    subprocess.check_call([cmd] + args, stderr=out, stdout=out, cwd=path)
-            except subprocess.CalledProcessError:
-                raise VcsError("%s error on %s. Command: %s" % (cmd, path, ' '.join([cmd] + args)))
-
-
-    def _path_contains(self, parent, path):
-        """Checks wether path is an object belonging to the subtree in parent"""
-        if parent == path: return True
-        parent = os.path.normpath(parent + '/')
-        path = os.path.normpath(path)
-        return os.path.commonprefix([parent, path]) == parent
-
-
-    # Object manipulation
-    #---------------------------
-    # This may be a little hacky, but very useful. An instance of Vcs class changes its own class
-    # when calling update(), to match the right repo type. I can have the same object adapt to
-    # the path repo type, if this ever changes!
-
-    def get_repo_type(self, path):
-        """Returns the right repo type for path. None if no repo present in path"""
-        for rn, rt in self.repo_types.items():
-            if path and os.path.exists(os.path.join(path, '.%s' % rn)): return rt
-        return None
-
-
-    def get_root(self, path):
-        """Finds the repository root path. Otherwise returns none"""
-        curpath = os.path.abspath(path)
-        while curpath != '/':
-            if self.get_repo_type(curpath): return curpath
-            else:                           curpath = os.path.dirname(curpath)
-        return None
-
-
-    def update(self, vcstype=None):
-        """Updates the repo instance. Re-checks the repo and changes object class if repo type changes
-           If vcstype is given, uses that repo type, without autodetection"""
-        if os.path.exists(self.path):
-            self.root = self.get_root(self.path)
-            if vcstype:
-                if vcstype in self.repo_types:
-                    ty = self.repo_types[vcstype]
+                    output = subprocess.check_output(cmd, cwd=path, stderr=devnull)
+                    return output if retbytes else output.decode('UTF-8')
                 else:
-                    raise VcsError("Unrecognized repo type %s" % vcstype)
-            else:
-                ty = self.get_repo_type(self.root)
-            if ty:
-                self.__class__ = ty
-                return
-
-        self.__class__ = Vcs
-
-
-    # Repo creation
-    #---------------------------
-
-    def init(self, repotype):
-        """Initializes a repo in current path"""
-        if not repotype in self.repo_types:
-            raise VcsError("Unrecognized repo type %s" % repotype)
-
-        if not os.path.exists(self.path): os.makedirs(self.path)
-        rt = self.repo_types[repotype]
-        try:
-            self.__class__ = rt
-            self.init()
-        except:
-            self.__class__ = Vcs
-            raise
-
-
-    def clone(self, repotype, src):
-        """Clones a repo from src"""
-        if not repotype in self.repo_types:
-            raise VcsError("Unrecognized repo type %s" % repotype)
-
-        if not os.path.exists(self.path): os.makedirs(self.path)
-        rt = self.repo_types[repotype]
-        try:
-            self.__class__ = rt
-            self.clone(src)
-        except:
-            self.__class__ = Vcs
-            raise
-
+                    subprocess.check_call(cmd, cwd=path, stdout=devnull, stderr=devnull)
+            except (subprocess.CalledProcessError, FileNotFoundError):
+                raise VcsError('{0:s}: {1:s}'.format(str(cmd), path))
+
+    def _get_repotype(self, path):
+        """Get type for path"""
+        for repotype in self.repotypes_settings:
+            repodir = os.path.join(path, '.' + repotype)
+            if os.path.exists(repodir):
+                return (repodir, repotype)
+        return (None, None)
+
+    def _find_root(self, path):
+        """Finds root path"""
+        links = set()
+        while True:
+            if os.path.islink(path):
+                links.add(path)
+                relpath = os.path.relpath(self.path, path)
+                path = os.path.realpath(path)
+                self.path = os.path.normpath(os.path.join(path, relpath))
+
+            repodir, repotype = self._get_repotype(path)
+            if repodir:
+                return (path, repodir, repotype, links)
+
+            path_old = path
+            path = os.path.dirname(path)
+            if path == path_old:
+                break
+
+        return (None, None, None, None)
+
+    def reinit(self):
+        """Reinit"""
+        if not self.in_repodir:
+            if not self.track \
+                    or (not self.is_root_pointer and self._get_repotype(self.obj.realpath)[0]) \
+                    or not os.path.exists(self.repodir):
+                self.__init__(self.obj)
 
     # Action interface
-    #---------------------------
-
-    def commit(self, message):
-        """Commits with a given message"""
-        raise NotImplementedError
-
 
-    def add(self, filelist):
-        """Adds files to the index, preparing for commit"""
+    def action_add(self, filelist):
+        """Adds files to the index"""
         raise NotImplementedError
 
-
-    def reset(self, filelist):
+    def action_reset(self, filelist):
         """Removes files from the index"""
         raise NotImplementedError
 
+    # Data interface
 
-    def pull(self):
-        """Pulls from remote"""
-        raise NotImplementedError
-
-
-    def push(self):
-        """Pushes to remote"""
-        raise NotImplementedError
-
-
-    def checkout(self, rev):
-        """Checks out a branch or revision"""
-        raise NotImplementedError
-
-
-    def extract_file(self, rev, name, dest):
-        """Extracts a file from a given revision and stores it in dest dir"""
-        raise NotImplementedError
-
-
-    # Data
-    #---------------------------
-
-    def is_repo(self):
-        """Checks wether there is an initialized repo in self.path"""
-        return self.path and os.path.exists(self.path) and self.root != None
-
-
-    def is_tracking(self):
-        """Checks whether HEAD is tracking a remote repo"""
-        return self.get_remote(self.HEAD) != None
-
-
-    def get_file_status(self, path):
-        """Returns the status for a given path regarding the repo"""
-
-        # if path is relative, join it with root. otherwise do nothing
-        path = os.path.join(self.root, path)
-
-        # path is not in the repo
-        if not self._path_contains(self.root, path):
-            return "none"
-
-        # check if prel or some parent of prel is ignored
-        prel = os.path.relpath(path, self.root)
-        while len(prel) > 0 and prel != '/' and prel != '.':
-            if prel in self.ignored: return "ignored"
-            prel, tail = os.path.split(prel)
-
-        # check if prel or some parent of prel is listed in status
-        prel = os.path.relpath(path, self.root)
-        while len(prel) > 0 and prel != '/' and prel != '.':
-            if prel in self.status: return self.status[prel]
-            prel, tail = os.path.split(prel)
-
-        # check if prel is a directory that contains some file in status
-        prel = os.path.relpath(path, self.root)
-        if os.path.isdir(path):
-            sts = set(st for p, st in self.status.items()
-                      if self._path_contains(path, os.path.join(self.root, p)))
-            for st in self.FILE_STATUS:
-                if st in sts: return st
-
-        # it seems prel is in sync
-        return "sync"
-
-
-    def get_status(self, path=None):
-        """Returns a dict with changed files under path and their status.
-           If path is None, returns all changed files"""
-
-        self.status = self.get_status_allfiles()
-        self.ignored = self.get_ignore_allfiles()
-        if path:
-            path = os.path.join(self.root, path)
-            if os.path.commonprefix([self.root, path]) == self.root:
-                return dict((p, st) for p, st in self.status.items() if self._path_contains(path, os.path.join(self.root, p)))
-            else:
-                return {}
-        else:
-            return self.status
-
-
-    def get_status_allfiles(self):
-        """Returns a dict indexed by files not in sync their status as values.
-           Paths are given relative to the root.  Strips trailing '/' from dirs."""
+    def data_status_root(self):
+        """Returns status of self.root cheaply"""
         raise NotImplementedError
 
-
-    def get_ignore_allfiles(self):
-        """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs."""
+    def data_status_subpaths(self):
+        """
+        Returns a dict indexed by subpaths not in sync with their status as values.
+        Paths are given relative to self.root
+        """
         raise NotImplementedError
 
-
-    def get_remote_status(self):
-        """Checks the status of the entire repo"""
+    def data_status_remote(self):
+        """
+        Returns remote status of repository
+        One of ('sync', 'ahead', 'behind', 'diverged', 'none')
+        """
         raise NotImplementedError
 
-
-    def get_branch(self):
+    def data_branch(self):
         """Returns the current named branch, if this makes sense for the backend. None otherwise"""
         raise NotImplementedError
 
-
-    def get_log(self):
-        """Get the entire log for the current HEAD"""
-        raise NotImplementedError
-
-
-    def get_raw_log(self, filelist=None):
-        """Gets the raw log as a string"""
-        raise NotImplementedError
-
-
-    def get_raw_diff(self, refspec=None, filelist=None):
-        """Gets the raw diff as a string"""
-        raise NotImplementedError
-
-
-    def get_remote(self):
-        """Returns the url for the remote repo attached to head"""
-        raise NotImplementedError
-
-
-    def get_revision_id(self, rev=None):
-        """Get a canonical key for the revision rev"""
+    def data_info(self, rev=None):
+        """Returns info string about revision rev. None in special cases"""
         raise NotImplementedError
 
 
-    def get_info(self, rev=None):
-        """Gets info about the given revision rev"""
-        raise NotImplementedError
+class VcsRoot(Vcs):  # pylint: disable=abstract-method
+    """Vcs root"""
+    rootinit = False
+    head = None
+    branch = None
+    updatetime = None
+    status_subpaths = None
 
+    def _status_root(self):
+        """Returns root status"""
+        if self.status_subpaths is None:
+            return 'none'
 
-    def get_files(self, rev=None):
-        """Gets a list of files in revision rev"""
-        raise NotImplementedError
+        statuses = set(status for path, status in self.status_subpaths.items())
+        for status in self.DIRSTATUSES:
+            if status in statuses:
+                return status
+        return 'sync'
 
+    def init_root(self):
+        """Initialize root cheaply"""
+        try:
+            self.head = self.data_info(self.HEAD)
+            self.branch = self.data_branch()
+            self.obj.vcsremotestatus = self.data_status_remote()
+            self.obj.vcsstatus = self.data_status_root()
+        except VcsError:
+            return False
+        self.rootinit = True
+        return True
+
+    def update_root(self):
+        """Update root state"""
+        try:
+            self.head = self.data_info(self.HEAD)
+            self.branch = self.data_branch()
+            self.status_subpaths = self.data_status_subpaths()
+            self.obj.vcsremotestatus = self.data_status_remote()
+            self.obj.vcsstatus = self._status_root()
+        except VcsError:
+            return False
+        self.rootinit = True
+        self.updatetime = time.time()
+        return True
+
+    def _update_walk(self, path, purge):  # pylint: disable=too-many-branches
+        """Update walk"""
+        for wroot, wdirs, _ in os.walk(path):
+            # Only update loaded directories
+            try:
+                wrootobj = self.obj.fm.directories[wroot]
+            except KeyError:
+                wdirs[:] = []
+                continue
+            if not wrootobj.vcs.track:
+                wdirs[:] = []
+                continue
+
+            if wrootobj.content_loaded:
+                has_vcschild = False
+                for fsobj in wrootobj.files_all:
+                    if purge:
+                        if fsobj.is_directory:
+                            fsobj.vcsstatus = None
+                            fsobj.vcs.__init__(fsobj)
+                        else:
+                            fsobj.vcsstatus = None
+                        continue
+
+                    if fsobj.is_directory:
+                        fsobj.vcs.reinit()
+                        if not fsobj.vcs.track:
+                            continue
+                        if fsobj.vcs.is_root_pointer:
+                            has_vcschild = True
+                        else:
+                            fsobj.vcsstatus = self.status_subpath(
+                                os.path.join(wrootobj.realpath, fsobj.basename),
+                                is_directory=True,
+                            )
+                    else:
+                        fsobj.vcsstatus = self.status_subpath(
+                            os.path.join(wrootobj.realpath, fsobj.basename))
+                wrootobj.has_vcschild = has_vcschild
+
+            # Remove dead directories
+            for wdir in list(wdirs):
+                try:
+                    wdirobj = self.obj.fm.directories[os.path.join(wroot, wdir)]
+                except KeyError:
+                    wdirs.remove(wdir)
+                    continue
+                if not wdirobj.vcs.track or wdirobj.vcs.is_root_pointer:
+                    wdirs.remove(wdir)
+
+    def update_tree(self, purge=False):
+        """Update tree state"""
+        self._update_walk(self.path, purge)
+        for path in list(self.links):
+            self._update_walk(path, purge)
+            try:
+                dirobj = self.obj.fm.directories[path]
+            except KeyError:
+                self.links.remove(path)
+                continue
+            if purge:
+                dirobj.vcsstatus = None
+                dirobj.vcs.__init__(dirobj)
+            elif dirobj.vcs.path == self.path:
+                dirobj.vcsremotestatus = self.obj.vcsremotestatus
+                dirobj.vcsstatus = self.obj.vcsstatus
+        if purge:
+            self.__init__(self.obj)
+
+    def check_outdated(self):
+        """Check if root is outdated"""
+        if self.updatetime is None:
+            return True
+
+        for wroot, wdirs, _ in os.walk(self.path):
+            wrootobj = self.obj.fm.get_directory(wroot)
+            wrootobj.load_if_outdated()
+            if wroot != self.path and wrootobj.vcs.is_root_pointer:
+                wdirs[:] = []
+                continue
+
+            if wrootobj.stat and self.updatetime < wrootobj.stat.st_mtime:
+                return True
+            if wrootobj.files_all:
+                for wfile in wrootobj.files_all:
+                    if wfile.stat and self.updatetime < wfile.stat.st_mtime:
+                        return True
+        return False
+
+    def status_subpath(self, path, is_directory=False):
+        """
+        Returns the status of path
 
+        path needs to be self.obj.path or subpath thereof
+        """
+        if self.status_subpaths is None:
+            return 'none'
+
+        relpath = os.path.relpath(path, self.path)
+
+        # check if relpath or its parents has a status
+        tmppath = relpath
+        while tmppath:
+            if tmppath in self.status_subpaths:
+                return self.status_subpaths[tmppath]
+            tmppath = os.path.dirname(tmppath)
+
+        # check if path contains some file in status
+        if is_directory:
+            statuses = set(status for subpath, status in self.status_subpaths.items()
+                           if subpath.startswith(relpath + '/'))
+            for status in self.DIRSTATUSES:
+                if status in statuses:
+                    return status
+        return 'sync'
+
+
+class VcsThread(threading.Thread):  # pylint: disable=too-many-instance-attributes
+    """VCS thread"""
+    def __init__(self, ui):
+        super(VcsThread, self).__init__()
+        self.daemon = True
+        self.ui = ui  # pylint: disable=invalid-name
+        self.queue = queue.Queue()
+        self.awoken = threading.Event()
+        self.timestamp = time.time()
+        self.redraw = False
+        self.roots = set()
+
+    def _is_targeted(self, dirobj):
+        """Check if dirobj is targeted"""
+        if self.ui.browser.main_column and self.ui.browser.main_column.target == dirobj:
+            return True
+        return False
+
+    def _update_subroots(self, fsobjs):
+        """Update subroots"""
+        if not fsobjs:
+            return False
+
+        has_vcschild = False
+        for fsobj in fsobjs:
+            if not fsobj.is_directory or not fsobj.vcs or not fsobj.vcs.track:
+                continue
+
+            rootvcs = fsobj.vcs.rootvcs
+            if fsobj.vcs.is_root_pointer:
+                has_vcschild = True
+                if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj):
+                    self.roots.add(rootvcs.path)
+                    if not rootvcs.init_root():
+                        rootvcs.update_tree(purge=True)
+                    self.redraw = True
+                if fsobj.is_link:
+                    fsobj.vcsstatus = rootvcs.obj.vcsstatus
+                    fsobj.vcsremotestatus = rootvcs.obj.vcsremotestatus
+                    self.redraw = True
+
+        return has_vcschild
+
+    def _queue_process(self):  # pylint: disable=too-many-branches
+        """Process queue"""
+        dirobjs = []
+        paths = set()
+        self.roots.clear()
+
+        while True:
+            try:
+                dirobjs.append(self.queue.get(block=False))
+            except queue.Empty:
+                break
+
+        for dirobj in dirobjs:
+            if dirobj.path in paths:
+                continue
+            paths.add(dirobj.path)
+
+            dirobj.vcs.reinit()
+            if dirobj.vcs.track:
+                rootvcs = dirobj.vcs.rootvcs
+                if rootvcs.path not in self.roots and rootvcs.check_outdated():
+                    self.roots.add(rootvcs.path)
+                    if rootvcs.update_root():
+                        rootvcs.update_tree()
+                    else:
+                        rootvcs.update_tree(purge=True)
+                    self.redraw = True
+
+            has_vcschild = self._update_subroots(dirobj.files_all)
+
+            if dirobj.has_vcschild != has_vcschild:
+                dirobj.has_vcschild = has_vcschild
+                self.redraw = True
+
+    def run(self):
+        while True:
+            self.awoken.wait()
+            self.awoken.clear()
+
+            self._queue_process()
+
+            if self.redraw:
+                self.redraw = False
+                for column in self.ui.browser.columns:
+                    if column.target and column.target.is_directory:
+                        column.need_redraw = True
+                self.ui.status.need_redraw = True
+                self.ui.redraw()
+
+    def wakeup(self, dirobj):
+        """Wakeup thread"""
+        self.queue.put(dirobj)
+        self.awoken.set()
+
+
+# Backend imports
+from .bzr import Bzr  # NOQA pylint: disable=wrong-import-position
+from .git import Git  # NOQA pylint: disable=wrong-import-position
+from .hg import Hg  # NOQA pylint: disable=wrong-import-position
+from .svn import SVN  # NOQA pylint: disable=wrong-import-position
+
+
+class BzrRoot(VcsRoot, Bzr):
+    """Bzr root"""
+    pass
 
-    # I / O
-    #---------------------------
 
-    def print_log(self, fmt):
-        log = self.log()
-        if fmt == "compact":
-            for dt in log:
-                print(self.format_revision_compact(dt))
-        else:
-            raise Exception("Unknown format %s" % fmt)
+class GitRoot(VcsRoot, Git):
+    """Git root"""
+    pass
 
 
-    def format_revision_compact(self, dt):
-        return "{0:<10}{1:<20}{2}".format(dt['revshort'],
-                                          dt['date'].strftime('%a %b %d, %Y'),
-                                          dt['summary'])
+class HgRoot(VcsRoot, Hg):
+    """Hg root"""
+    pass
 
 
-    def format_revision_text(self, dt):
-        L = ["revision:         %s:%s" % (dt['revshort'], dt['revhash']),
-             "date:             %s" % dt['date'].strftime('%a %b %d, %Y'),
-             "time:             %s" % dt['date'].strftime('%H:%M'),
-             "user:             %s" % dt['author'],
-             "description:      %s" % dt['summary']]
-        return '\n'.join(L)
+class SVNRoot(VcsRoot, SVN):
+    """SVN root"""
+    pass
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index e5aef06c..ac597e5a 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -17,9 +17,9 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo',
         'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded',
         'keybuffer',
         'infostring',
-        'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit',
+        'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsdate',
         'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored',
-        'vcsstaged', 'vcssync', 'vcsbehind', 'vcsahead', 'vcsdiverged']
+        'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged']
 
 class Context(object):
     def __init__(self, keys):
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 2eacbc4d..41b57c2c 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -5,10 +5,12 @@ import os
 import sys
 import curses
 import _curses
+import threading
 
 from .displayable import DisplayableContainer
 from .mouse_event import MouseEvent
 from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY
+from ranger.ext.lazy_property import lazy_property
 
 MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 
@@ -40,6 +42,8 @@ class UI(DisplayableContainer):
     def __init__(self, env=None, fm=None):
         self.keybuffer = KeyBuffer()
         self.keymaps = KeyMaps(self.keybuffer)
+        self.redrawlock = threading.Event()
+        self.redrawlock.set()
 
         if fm is not None:
             self.fm = fm
@@ -248,8 +252,18 @@ class UI(DisplayableContainer):
         self.pager.visible = False
         self.add_child(self.pager)
 
+    @lazy_property
+    def vcsthread(self):
+        """VCS thread"""
+        from ranger.ext.vcs import VcsThread
+        thread = VcsThread(self)
+        thread.start()
+        return thread
+
     def redraw(self):
         """Redraw all widgets"""
+        self.redrawlock.wait()
+        self.redrawlock.clear()
         self.poke()
 
         # determine which widgets are shown
@@ -264,6 +278,7 @@ class UI(DisplayableContainer):
 
         self.draw()
         self.finalize()
+        self.redrawlock.set()
 
     def redraw_window(self):
         """Redraw the window. This only calls self.win.redrawwin()."""
diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py
index bd0f2337..f61e18eb 100644
--- a/ranger/gui/widgets/__init__.py
+++ b/ranger/gui/widgets/__init__.py
@@ -5,21 +5,25 @@ from ranger.gui.displayable import Displayable
 class Widget(Displayable):
     """A class for classification of widgets."""
 
-    vcsfilestatus_symb = {'conflict':  ('X', ["vcsconflict"]),
-                  'untracked': ('+', ["vcschanged"]),
-                  'deleted':   ('-', ["vcschanged"]),
-                  'changed':   ('*', ["vcschanged"]),
-                  'staged':    ('*', ["vcsstaged"]),
-                  'ignored':   ('·', ["vcsignored"]),
-                  'sync':      ('√', ["vcssync"]),
-                  'none':      (' ', []),
-                  'unknown':   ('?', ["vcsunknown"])}
+    vcsstatus_symb = {
+        'conflict':  ('X', ['vcsconflict']),
+        'untracked': ('+', ['vcschanged']),
+        'deleted':   ('-', ['vcschanged']),
+        'changed':   ('*', ['vcschanged']),
+        'staged':    ('*', ['vcsstaged']),
+        'ignored':   ('·', ['vcsignored']),
+        'sync':      ('√', ['vcssync']),
+        'none':      (' ', []),
+        'unknown':   ('?', ['vcsunknown']),
+    }
 
-    vcsremotestatus_symb = {'none':     (' ',  []),
-                'sync':     ('=', ["vcssync"]),
-                'behind':   ('<', ["vcsbehind"]),
-                'ahead':    ('>', ["vcsahead"]),
-                'diverged': ('Y', ["vcsdiverged"]),
-                'unknown':  ('?', ["vcsunknown"])}
+    vcsremotestatus_symb = {
+        'diverged': ('Y', ['vcsdiverged']),
+        'ahead':    ('>', ['vcsahead']),
+        'behind':   ('<', ['vcsbehind']),
+        'sync':     ('=', ['vcssync']),
+        'none':     ('⌂', ['vcsnone']),
+        'unknown':  ('?', ['vcsunknown']),
+    }
 
-    ellipsis = { False: '~', True: '…' }
+    ellipsis = {False: '~', True: '…'}
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 52ef62b8..07830b31 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -150,8 +150,8 @@ class BrowserColumn(Pager):
                 self.old_thisfile = self.target.pointed_obj
 
             if self.target.load_content_if_outdated() \
-            or self.target.sort_if_outdated() \
-            or self.last_redraw_time < self.target.last_update_time:
+                    or self.target.sort_if_outdated() \
+                    or self.last_redraw_time < self.target.last_update_time:
                 self.need_redraw = True
 
         if self.need_redraw:
@@ -256,9 +256,9 @@ class BrowserColumn(Pager):
 
             metakey = hash(repr(sorted(metadata.items()))) if metadata else 0
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
-                    drawn.path in copied, tagged_marker, drawn.infostring,
-                    drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut,
-                    current_linemode.name, metakey)
+                   drawn.path in copied, tagged_marker, drawn.infostring,
+                   drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild,
+                   self.fm.do_cut, current_linemode.name, metakey)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
@@ -372,25 +372,21 @@ class BrowserColumn(Pager):
 
     def _draw_vcsstring_display(self, drawn):
         vcsstring_display = []
-        if self.settings.vcs_aware and (drawn.vcsfilestatus or \
-                drawn.vcsremotestatus):
-            if drawn.vcsfilestatus:
-                vcsstr, vcscol = self.vcsfilestatus_symb[drawn.vcsfilestatus]
-            else:
-                vcsstr = " "
-                vcscol = []
-            vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
-
+        if (self.target.vcs and self.target.vcs.track) \
+                or (drawn.is_directory and drawn.vcs and drawn.vcs.track):
             if drawn.vcsremotestatus:
-                vcsstr, vcscol = self.vcsremotestatus_symb[
-                        drawn.vcsremotestatus]
-            else:
-
-                vcsstr = " "
-                vcscol = []
-            vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol])
+                vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcsremotestatus]
+                vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol])
+            elif self.target.has_vcschild:
+                vcsstring_display.append([' ', []])
+            if drawn.vcsstatus:
+                vcsstr, vcscol = self.vcsstatus_symb[drawn.vcsstatus]
+                vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
+            elif self.target.has_vcschild:
+                vcsstring_display.append([' ', []])
         elif self.target.has_vcschild:
-            vcsstring_display.append(["  ", []])
+            vcsstring_display.append(['  ', []])
+
         return vcsstring_display
 
     def _draw_directory_color(self, i, drawn, copied):
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index ee4df028..33aa4296 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -178,28 +178,32 @@ class StatusBar(Widget):
                 left.add(target.infostring.replace(" ", ""))
                 left.add_space()
 
-            left.add(strftime(self.timeformat,
-                    localtime(stat.st_mtime)), 'mtime')
-
-        if target.vcs:
-            if target.vcsbranch:
-                vcsinfo = '(%s: %s)' % (target.vcs.vcsname, target.vcsbranch)
+            left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime')
+
+        directory = target if target.is_directory else \
+            target.fm.get_directory(os.path.dirname(target.path))
+        if directory.vcs and directory.vcs.track:
+            if directory.vcs.rootvcs.branch:
+                vcsinfo = '({0:s}: {1:s})'.format(
+                    directory.vcs.rootvcs.repotype, directory.vcs.rootvcs.branch)
             else:
-                vcsinfo = '(%s)' % (target.vcs.vcsname)
-
+                vcsinfo = '({0:s})'.format(directory.vcs.rootvcs.repotype)
             left.add_space()
             left.add(vcsinfo, 'vcsinfo')
 
-            if target.vcsfilestatus:
-                left.add_space()
-                vcsstr, vcscol = self.vcsfilestatus_symb[target.vcsfilestatus]
-                left.add(vcsstr.strip(), 'vcsfile', *vcscol)
-            if target.vcsremotestatus:
-                vcsstr, vcscol = self.vcsremotestatus_symb[target.vcsremotestatus]
+            left.add_space()
+            if directory.vcs.rootvcs.obj.vcsremotestatus:
+                vcsstr, vcscol = self.vcsremotestatus_symb[
+                    directory.vcs.rootvcs.obj.vcsremotestatus]
                 left.add(vcsstr.strip(), 'vcsremote', *vcscol)
-            if target.vcshead:
+            if target.vcsstatus:
+                vcsstr, vcscol = self.vcsstatus_symb[target.vcsstatus]
+                left.add(vcsstr.strip(), 'vcsfile', *vcscol)
+            if directory.vcs.rootvcs.head:
+                left.add_space()
+                left.add(directory.vcs.rootvcs.head['date'].strftime(self.timeformat), 'vcsdate')
                 left.add_space()
-                left.add('%s' % target.vcshead['summary'], 'vcscommit')
+                left.add(directory.vcs.rootvcs.head['summary'], 'vcscommit')
 
     def _get_owner(self, target):
         uid = target.stat.st_uid