summary refs log tree commit diff stats
diff options
context:
space:
mode:
authornfnty <git@nfnty.se>2015-10-05 14:50:23 +0200
committernfnty <git@nfnty.se>2016-02-08 04:43:03 +0100
commit96dd8db40de076e751adcba65931b981438c1de2 (patch)
treea665dd34912fd5c406fc0b2194083a99089a39a6
parente7e867685eaebb0d12e9e69b74b18cb565b84f10 (diff)
downloadranger-96dd8db40de076e751adcba65931b981438c1de2.tar.gz
VCS: Git
-rw-r--r--ranger/container/directory.py28
-rw-r--r--ranger/container/fsobject.py83
-rw-r--r--ranger/ext/vcs/__init__.py4
-rw-r--r--ranger/ext/vcs/bzr.py2
-rw-r--r--ranger/ext/vcs/git.py312
-rw-r--r--ranger/ext/vcs/hg.py2
-rw-r--r--ranger/ext/vcs/svn.py2
-rw-r--r--ranger/ext/vcs/vcs.py351
-rw-r--r--ranger/gui/widgets/browsercolumn.py22
-rw-r--r--ranger/gui/widgets/statusbar.py21
10 files changed, 362 insertions, 465 deletions
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 52b494d5..486507b9 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.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,9 +306,9 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                 files = []
                 disk_usage = 0
 
-                if self.settings.vcs_aware:
-                    self.has_vcschild = False
-                    self.load_vcs(None)
+                if self.settings.vcs_aware and self.vcs.root:
+                    self.has_vcschild = True
+                    self.vcs.update(self)
 
                 for name in filenames:
                     try:
@@ -329,23 +334,24 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                             except:
                                 item = Directory(name, preload=stats, path_is_abs=True)
                                 item.load()
+                        if item.settings.vcs_aware:
+                            if item.vcs.is_root:
+                                self.has_vcschild = True
+                                item.vcs.update(item)
+                            elif item.vcs.root:
+                                item.vcs.update_child(item)
                     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.settings.vcs_aware and self.vcs.root:
+                            item.vcsfilestatus = self.vcs.get_path_status(item.path)
 
                     files.append(item)
                     self.percent = 100 * len(files) // len(filenames)
                     yield
                 self.disk_usage = disk_usage
-                self.vcs_outdated = False
 
                 self.filenames = filenames
                 self.files_all = files
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index aa848b7a..e396a75a 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -73,14 +73,7 @@ class FileSystemObject(FileManagerAware, SettingsAware):
 
     size = 0
 
-    (vcs,
-     vcsfilestatus,
-     vcsremotestatus,
-     vcsbranch,
-     vcshead) = (None,) * 5
-
-    vcs_outdated = False
-    vcs_enabled = False
+    vcsfilestatus = None
 
     basename_is_rel_to = None
 
@@ -241,78 +234,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 +332,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..a1e12a7a 100644
--- a/ranger/ext/vcs/__init__.py
+++ b/ranger/ext/vcs/__init__.py
@@ -5,8 +5,6 @@
 #
 # vcs - a python module to handle various version control systems
 
-import os
-
-from .vcs import VcsError, Vcs
+from ranger.ext.vcs.vcs import VcsError, Vcs
 
 # vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py
index 2a52cf02..c8c4ff4e 100644
--- a/ranger/ext/vcs/bzr.py
+++ b/ranger/ext/vcs/bzr.py
@@ -10,7 +10,7 @@ import re
 import shutil
 from datetime import datetime
 
-from .vcs import Vcs, VcsError
+from ranger.ext.vcs import Vcs, VcsError
 
 
 class Bzr(Vcs):
diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py
index bba1bfee..86a42502 100644
--- a/ranger/ext/vcs/git.py
+++ b/ranger/ext/vcs/git.py
@@ -9,187 +9,176 @@ import os
 import re
 import shutil
 from datetime import datetime
+import json
 
-from .vcs import Vcs, VcsError
-
+from ranger.ext.vcs import Vcs, VcsError
 
 class Git(Vcs):
-    vcsname  = 'git'
+    """VCS implementation for Git"""
+    vcsname = 'git'
+    _status_combinations = (
+        ('MADRC', ' ', 'staged'),
+        (' MADRC', 'M', 'changed'),
+        (' MARC', 'D', 'deleted'),
+
+        ('D', 'DU', 'conflict'),
+        ('A', 'AU', 'conflict'),
+        ('U', 'ADU', 'conflict'),
+
+        ('?', '?', 'untracked'),
+        ('!', '!', 'ignored'),
+    )
 
     # Auxiliar stuff
     #---------------------------
 
-    def _git(self, path, args, silent=True, catchout=False, bytes=False):
-        return self._vcs(path, 'git', args, silent=silent, catchout=catchout, bytes=bytes)
-
+    def _git(self, args, path=None, silent=True, catchout=False, bytes=False):
+        """Call git"""
+        return self._vcs(path if path else self.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)
+            self._git(['rev-parse', 'HEAD'], silent=True)
         except VcsError:
             return False
         return True
 
-
     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
-
+        return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) 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
-
+        if ref is None:
+            return None
+        return self._git(['for-each-ref', '--format=%(upstream)', ref],
+                         catchout=True, silent=True) \
+            or None
 
     def _sanitize_rev(self, rev):
-        if rev == None: return None
+        """Sanitize revision string"""
+        if rev is None:
+            return None
         return rev.strip()
 
-
     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
-
-        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)
+        args = [
+            '--no-pager', 'log',
+            '--pretty={"short": "%h", "revid": "%H", "author": "%an", "date": %ct, "summary": "%s"}'
+        ]
+        if refspec:
+            args += ['-1', refspec]
+        elif maxres:
+            args += ['-{0:d}'.format(maxres)]
+        if filelist:
+            args += ['--'] + filelist
 
         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 self._git(args, catchout=True).splitlines():
+            line = json.loads(line)
+            line['date'] = datetime.fromtimestamp(line['date'])
+            log.append(line)
         return log
 
-
-    def _git_file_status(self, status):
-        """ Translate git status code """
-        X, Y = (status[0], status[1])
-
-        if   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 "D"      and Y in "DU"   : return 'conflict'
-        elif X in "A"      and Y in "AU"   : return 'conflict'
-        elif X in "U"      and Y in "ADU"  : return 'conflict'
-
-        elif X in "?"      and Y in "?"    : return 'untracked'
-        elif X in "!"      and Y in "!"    : return 'ignored'
-
-        else                               : return 'unknown'
-
-
+    def _git_file_status(self, code):
+        """Translate git status code"""
+        for X, Y, status in self._status_combinations:
+            if code[0] in X and code[1] in Y:
+                return status
+        return 'unknown'
 
     # Repo creation
     #---------------------------
 
     def init(self):
         """Initializes a repo in current path"""
-        self._git(self.path, ['init'])
+        self._git(['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)
+            raise VcsError("Can't clone to {0:s}: Not an empty directory".format(self.path))
 
-        self._git(path, ['clone', src, name])
+        self._git(['clone', src, os.path.basename(self.path)], path=os.path.dirname(self.path))
         self.update()
 
-
-
     # Action interface
     #---------------------------
 
     def commit(self, message):
         """Commits with a given message"""
-        self._git(self.path, ['commit', '-m', message])
-
+        self._git(['commit', '--message', 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'])
-
+        if filelist:
+            self._git(['add', '--all'] + filelist)
+        else:
+            self._git(['add', '--all'])
 
     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'])
-
+        if filelist:
+            self._git(['reset'] + filelist)
+        else:
+            self._git(['reset'])
 
-    def pull(self, br=None):
+    def pull(self, *args):
         """Pulls from remote"""
-        if br: self._git(self.path, ['pull', br])
-        else:  self._git(self.path, ['pull'])
-
+        self._git(['pull'] + list(args))
 
-    def push(self, br=None):
+    def push(self, *args):
         """Pushes to remote"""
-        if br: self._git(self.path, ['push', br])
-        else:  self._git(self.path, ['push'])
-
+        self._git(['push'] + list(args))
 
     def checkout(self, rev):
         """Checks out a branch or revision"""
-        self._git(self.path, ['checkout', self._sanitize_rev(rev)])
-
+        self._git(['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)
-
-
+            with open(dest, 'wb') as fd:
+                fd.write(
+                    self._git([
+                        '--no-pager', 'show', '{0:s}:{1:s}'.format(self._sanitize_rev(rev), name)
+                    ], catchout=True, bytes=True)
+                )
 
     # Data Interface
     #---------------------------
 
     def get_status_allfiles(self):
-        """ Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs """
-        output = self._git(self.path, ['status', '--porcelain', '-z'], catchout=True, bytes=True)\
-            .decode('utf-8').split('\x00')[:-1]
-        output.reverse()
-        statuses = []
-        while output:
-            line = output.pop()
-            statuses.append((line[:2], line[3:]))
+        """Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs"""
+        statuses = {}
+        skip = False
+        for line in self._git(['status', '--porcelain', '-z'], catchout=True, bytes=True)\
+                .decode('utf-8').split('\x00')[:-1]:
+            if skip:
+                skip = False
+                continue
+            statuses[os.path.normpath(line[3:])] = self._git_file_status(line[:2])
             if line.startswith('R'):
-                output.pop()
-
-        return {os.path.normpath(tup[1]): self._git_file_status(tup[0]) for tup in statuses}
-
+                skip = True
+        return statuses
 
     def get_ignore_allfiles(self):
-        """ Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs. """
-        output = self._git(self.path, ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'],
-                           catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]
-        return set(os.path.normpath(p) for p in output)
-
+        """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs."""
+        return set(
+            os.path.normpath(p)
+            for p in self._git(
+                ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'],
+                catchout=True, bytes=True
+            ).decode('utf-8').split('\x00')[:-1]
+        )
 
     def get_remote_status(self):
         """Checks the status of the repo regarding sync state with remote branch"""
@@ -198,18 +187,17 @@ class Git(Vcs):
             remote = self._remote_ref(head)
         except VcsError:
             head = remote = None
-
-        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"
-
+        if not head or not remote:
+            return 'none'
+
+        output = self._git(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)],
+                           catchout=True)
+        ahead = re.search("^>", output, flags=re.MULTILINE)
+        behind = re.search("^<", output, flags=re.MULTILINE)
+        if ahead:
+            return 'diverged' if behind else 'ahead'
+        else:
+            return 'behind' if behind else 'sync'
 
     def get_branch(self):
         """Returns the current named branch, if this makes sense for the backend. None otherwise"""
@@ -217,37 +205,38 @@ class Git(Vcs):
             head = self._head_ref()
         except VcsError:
             head = None
+        if head is None:
+            return 'detached'
 
-        if head:
-            m = re.match('refs/heads/([^/]*)', head)
-            if m: return m.group(1).strip()
+        match = re.match('refs/heads/([^/]+)', head)
+        if match:
+            return match.group(1)
         else:
-            return "detached"
-
-        return None
-
+            return None
 
     def get_log(self, filelist=None, maxres=None):
         """Get the entire log for the current HEAD"""
-        if not self._has_head(): return []
+        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 []
+        if not self._has_head():
+            return []
         args = ['log']
-        if filelist: args = args + ['--'] + filelist
-        return self._git(self.path, args, catchout=True)
-
+        if filelist:
+            args += ['--'] + filelist
+        return self._git(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)
-
+        if refspec:
+            args += [refspec]
+        if filelist:
+            args += ['--'] + filelist
+        return self._git(args, catchout=True)
 
     def get_remote(self):
         """Returns the url for the remote repo attached to head"""
@@ -257,49 +246,56 @@ class Git(Vcs):
                 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
+            if not remote:
+                return None
+
+            match = re.match('refs/remotes/([^/]+)/', remote)
+            if match:
+                return self._git(['config', '--get', 'remote.{0:s}.url'.format(match.group(1))],
+                                 catchout=True).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
+        if rev is 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))
-
+        return self._sanitize_rev(self._git(['rev-parse', rev], catchout=True))
 
     def get_info(self, rev=None):
         """Gets info about the given revision rev"""
-        if rev == None: rev = self.HEAD
+        if rev is 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)
+        if rev == self.HEAD and not self._has_head():
+            return None
+
+        log = self._log(refspec=rev)
+        if len(log) == 0:
+            raise VcsError("Revision {0:s} does not exist".format(rev))
+        elif len(log) > 1:
+            raise VcsError("More than one instance of revision {0:s} ?!?".format(rev))
         else:
-            return L[0]
-
+            return log[0]
 
     def get_files(self, rev=None):
         """Gets a list of files in revision rev"""
-        if rev == None: rev = self.HEAD
+        if rev is None:
+            rev = self.HEAD
         rev = self._sanitize_rev(rev)
+        if rev is None:
+            return []
 
-        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 rev == self.INDEX:
+            return self._git(['ls-files', '-z'],
+                             catchout=True, bytes=True).decode('utf-8').split('\x00')
         else:
-            return []
+            return self._git(['ls-tree', '--name-only', '-r', '-z', rev],
+                             catchout=True, bytes=True).decode('utf-8').split('\x00')
 
 # vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py
index b8731dbf..35daff4a 100644
--- a/ranger/ext/vcs/hg.py
+++ b/ranger/ext/vcs/hg.py
@@ -14,7 +14,7 @@ try:
 except ImportError:
     from ConfigParser import RawConfigParser
 
-from .vcs import Vcs, VcsError
+from ranger.ext.vcs import Vcs, VcsError
 
 
 class Hg(Vcs):
diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py
index 9bf8698c..78dfb8e9 100644
--- a/ranger/ext/vcs/svn.py
+++ b/ranger/ext/vcs/svn.py
@@ -14,7 +14,7 @@ import re
 import shutil
 import logging
 from datetime import datetime
-from .vcs import Vcs, VcsError
+from ranger.ext.vcs import Vcs, VcsError
 
 #logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG,
 #                    filemode='w')
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index c37bf006..ac996ef4 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -4,16 +4,15 @@
 # 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
-
 
 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.
@@ -36,99 +35,144 @@ class Vcs(object):
     # 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}
+    INDEX = 'INDEX'
+    HEAD = 'HEAD'
+    NONE = 'NONE'
+    vcsname = None
+
+    # Possible status responses in order of importance
+    FILE_STATUS = (
+        'conflict',
+        'untracked',
+        'deleted',
+        'changed',
+        'staged',
+        'ignored',
+        'sync',
+        'none',
+        'unknown',
+    )
+    REMOTE_STATUS = (
+        'diverged',
+        'behind',
+        'ahead',
+        'sync',
+        'none',
+        'unknown',
+    )
+
+    def __init__(self, directoryobject):
+        from ranger.ext.vcs.git import Git
+        from ranger.ext.vcs.hg import Hg
+        from ranger.ext.vcs.bzr import Bzr
+        from ranger.ext.vcs.svn import SVN
+        self.repotypes = {
+            'git': Git,
+            'hg': Hg,
+            'bzr': Bzr,
+            'svn': SVN,
+        }
+
+        self.path = directoryobject.path
+        self.repotypes_settings = [
+            repotype for repotype, setting in \
+            (
+                ('git', directoryobject.settings.vcs_backend_git),
+                ('hg', directoryobject.settings.vcs_backend_git),
+                ('bzr', directoryobject.settings.vcs_backend_git),
+                ('svn', directoryobject.settings.vcs_backend_git),
+            )
+            if setting in ('enabled', 'local')
+        ]
 
-        self.path = os.path.expanduser(path)
         self.status = {}
         self.ignored = set()
-        self.root = None
-
-        self.update(vcstype=vcstype)
-
+        self.head = None
+        self.remotestatus = None
+        self.branch = None
+
+        self.root, self.repotype = self.find_root(self.path)
+        self.is_root = True if self.path == self.root else False
+
+        if self.root:
+            # Do not track the repo data directory
+            repodir = os.path.join(self.root, '.{0:s}'.format(self.repotype))
+            if self.path == repodir or self.path.startswith(repodir + '/'):
+                self.root = None
+                return
+            if self.is_root:
+                self.root = self.path
+                self.__class__ = self.repotypes[self.repotype]
+            else:
+                root = directoryobject.fm.get_directory(self.root)
+                self.repotype = root.vcs.repotype
+                self.__class__ = root.vcs.__class__
 
     # 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
+        with open(os.devnull, 'w') as devnull:
+            out = devnull if silent else None
             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()
+                    output = subprocess.check_output([cmd] + args, stderr=out, cwd=path)
+                    return output if bytes else output.decode('utf-8').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)))
+                raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\
+                               .format(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
+    # Generic
     #---------------------------
-    # 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):
+    def get_repotype(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
+        for repotype in self.repotypes_settings:
+            if os.path.exists(os.path.join(path, '.{0:s}'.format(repotype))):
+                return repotype
         return None
 
-
-    def get_root(self, path):
+    def find_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]
-                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
-
+        while True:
+            repotype = self.get_repotype(path)
+            if repotype:
+                return (path, repotype)
+            if path == '/':
+                break
+            path = os.path.dirname(path)
+        return (None, None)
+
+    def update(self, directoryobject):
+        """Update repository"""
+        if self.is_root:
+            self.head = self.get_info(self.HEAD)
+            self.branch = self.get_branch()
+            self.remotestatus = self.get_remote_status()
+            self.status = self.get_status_allfiles()
+            self.ignored = self.get_ignore_allfiles()
+            directoryobject.vcsfilestatus = self.get_root_status()
+        else:
+            root = directoryobject.fm.get_directory(self.root)
+            self.head = root.vcs.head = root.vcs.get_info(root.vcs.HEAD)
+            self.branch = root.vcs.branch = root.vcs.get_branch()
+            self.status = root.vcs.status = root.vcs.get_status_allfiles()
+            self.ignored = root.vcs.ignored = root.vcs.get_ignore_allfiles()
+            directoryobject.vcsfilestatus = root.vcs.get_path_status(
+                self.path, is_directory=True)
+
+    def update_child(self, directoryobject):
+        """After update() for subdirectories"""
+        root = directoryobject.fm.get_directory(self.root)
+        self.head = root.vcs.head
+        self.branch = root.vcs.branch
+        self.status = root.vcs.status
+        self.ignored = root.vcs.ignored
+        directoryobject.vcsfilestatus = root.vcs.get_path_status(
+            self.path, is_directory=True)
 
     # Repo creation
     #---------------------------
@@ -136,33 +180,31 @@ class Vcs(object):
     def init(self, repotype):
         """Initializes a repo in current path"""
         if not repotype in self.repo_types:
-            raise VcsError("Unrecognized repo type %s" % repotype)
+            raise VcsError("Unrecognized repo type {0:s}".format(repotype))
 
-        if not os.path.exists(self.path): os.makedirs(self.path)
-        rt = self.repo_types[repotype]
+        if not os.path.exists(self.path):
+            os.makedirs(self.path)
         try:
-            self.__class__ = rt
+            self.__class__ = self.repo_types[repotype]
             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)
+            raise VcsError("Unrecognized repo type {0:s}".format(repotype))
 
-        if not os.path.exists(self.path): os.makedirs(self.path)
-        rt = self.repo_types[repotype]
+        if not os.path.exists(self.path):
+            os.makedirs(self.path)
         try:
-            self.__class__ = rt
+            self.__class__ = self.repo_types[repotype]
             self.clone(src)
         except:
             self.__class__ = Vcs
             raise
 
-
     # Action interface
     #---------------------------
 
@@ -170,178 +212,117 @@ class Vcs(object):
         """Commits with a given message"""
         raise NotImplementedError
 
-
     def add(self, filelist):
         """Adds files to the index, preparing for commit"""
         raise NotImplementedError
 
-
     def reset(self, filelist):
         """Removes files from the index"""
         raise NotImplementedError
 
-
-    def pull(self):
+    def pull(self, **kwargs):
         """Pulls from remote"""
         raise NotImplementedError
 
-
-    def push(self):
+    def push(self, **kwargs):
         """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
-
+        return self.path and os.path.exists(self.path) and self.root is not 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
-        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
-
+        return self.get_remote(self.HEAD) is not None
+
+    def get_root_status(self):
+        """Returns the status of root"""
+        statuses = set(
+            status for path, status in self.status.items()
+        )
+        for status in self.FILE_STATUS:
+            if status in statuses:
+                return status
+        return 'sync'
+
+    def get_path_status(self, path, is_directory=False):
+        """Returns the status of path"""
+        relpath = os.path.relpath(path, self.root)
+
+        # check if relpath or its parents has a status
+        tmppath = relpath
+        while tmppath:
+            if tmppath in self.ignored:
+                return 'ignored'
+            elif tmppath in self.status:
+                return self.status[tmppath]
+            tmppath = os.path.dirname(tmppath)
+
+        # check if path contains some file in status
+        if is_directory:
+            statuses = set(
+                status for path, status in self.status.items()
+                if path.startswith(relpath + '/')
+            )
+            for status in self.FILE_STATUS:
+                if status in statuses:
+                    return status
+
+        return 'sync'
 
     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."""
         raise NotImplementedError
 
-
     def get_ignore_allfiles(self):
         """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs."""
         raise NotImplementedError
 
-
     def get_remote_status(self):
         """Checks the status of the entire repo"""
         raise NotImplementedError
 
-
     def get_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"""
         raise NotImplementedError
 
-
     def get_info(self, rev=None):
         """Gets info about the given revision rev"""
         raise NotImplementedError
 
-
     def get_files(self, rev=None):
         """Gets a list of files in revision rev"""
         raise NotImplementedError
-
-
-
-    # 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)
-
-
-    def format_revision_compact(self, dt):
-        return "{0:<10}{1:<20}{2}".format(dt['revshort'],
-                                          dt['date'].strftime('%a %b %d, %Y'),
-                                          dt['summary'])
-
-
-    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)
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 52ef62b8..8dff30d1 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -372,25 +372,17 @@ class BrowserColumn(Pager):
 
     def _draw_vcsstring_display(self, drawn):
         vcsstring_display = []
-        if self.settings.vcs_aware and (drawn.vcsfilestatus or \
-                drawn.vcsremotestatus):
+        directory = drawn if drawn.is_directory else self.target
+        if self.settings.vcs_aware and directory.vcs.root:
             if drawn.vcsfilestatus:
                 vcsstr, vcscol = self.vcsfilestatus_symb[drawn.vcsfilestatus]
-            else:
-                vcsstr = " "
-                vcscol = []
-            vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
-
-            if drawn.vcsremotestatus:
-                vcsstr, vcscol = self.vcsremotestatus_symb[
-                        drawn.vcsremotestatus]
-            else:
-
-                vcsstr = " "
-                vcscol = []
-            vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol])
+                vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
+            if drawn.is_directory and drawn.vcs.remotestatus:
+                vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus]
+                vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol])
         elif self.target.has_vcschild:
             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..d6f2c91c 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -181,11 +181,16 @@ class StatusBar(Widget):
             left.add(strftime(self.timeformat,
                     localtime(stat.st_mtime)), 'mtime')
 
-        if target.vcs:
-            if target.vcsbranch:
-                vcsinfo = '(%s: %s)' % (target.vcs.vcsname, target.vcsbranch)
+        if target.settings.vcs_aware:
+            if target.is_directory and target.vcs.root:
+                directory = target
             else:
-                vcsinfo = '(%s)' % (target.vcs.vcsname)
+                directory = target.fm.get_directory(os.path.dirname(target.path))
+
+            if directory.vcs.branch:
+                vcsinfo = '(%s: %s)' % (directory.vcs.vcsname, directory.vcs.branch)
+            else:
+                vcsinfo = '(%s)' % (directory.vcs.vcsname)
 
             left.add_space()
             left.add(vcsinfo, 'vcsinfo')
@@ -194,12 +199,12 @@ class StatusBar(Widget):
                 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]
+            if directory.vcs.remotestatus:
+                vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.remotestatus]
                 left.add(vcsstr.strip(), 'vcsremote', *vcscol)
-            if target.vcshead:
+            if directory.vcs.head:
                 left.add_space()
-                left.add('%s' % target.vcshead['summary'], 'vcscommit')
+                left.add('%s' % directory.vcs.head['summary'], 'vcscommit')
 
     def _get_owner(self, target):
         uid = target.stat.st_uid