summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.112
-rw-r--r--doc/ranger.pod12
-rw-r--r--ranger/colorschemes/default.py36
-rw-r--r--ranger/config/commands.py99
-rw-r--r--ranger/config/rc.conf10
-rw-r--r--ranger/container/settingobject.py4
-rw-r--r--ranger/ext/vcs/__init__.py24
-rw-r--r--ranger/ext/vcs/bzr.py282
-rw-r--r--ranger/ext/vcs/git.py315
-rw-r--r--ranger/ext/vcs/hg.py283
-rw-r--r--ranger/ext/vcs/vcs.py344
-rw-r--r--ranger/fsobject/directory.py39
-rw-r--r--ranger/fsobject/file.py3
-rw-r--r--ranger/fsobject/fsobject.py40
-rw-r--r--ranger/gui/context.py6
-rw-r--r--ranger/gui/widgets/__init__.py19
-rw-r--r--ranger/gui/widgets/browsercolumn.py202
-rw-r--r--ranger/gui/widgets/statusbar.py24
-rwxr-xr-xsetup.py3
19 files changed, 1674 insertions, 83 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1
index d23b96fd..9296084f 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -690,6 +690,18 @@ Set the title to \*(L"ranger\*(R" in the tmux program?
 .IP "use_preview_script [bool] <zv>" 4
 .IX Item "use_preview_script [bool] <zv>"
 Use the preview script defined in the setting \fIpreview_script\fR?
+.IP "vcs_aware [bool]" 4
+.IX Item "vcs_aware [bool]"
+Gather and display data about version control systems. Supported vcs: git, hg.
+.IP "vcs_backend_git, vcs_backend_hg, vcs_backend_bzr [string]" 4
+.IX Item "vcs_backend_git, vcs_backend_hg, vcs_backend_bzr [string]"
+Sets the state for the version control backend. The possible values are:
+.Sp
+.Vb 3
+\& disabled   don\*(Aqt display any information.
+\& local      display only local state.
+\& enabled    display both, local and remote state. May be slow for hg and bzr.
+.Ve
 .IP "xterm_alt_key [bool]" 4
 .IX Item "xterm_alt_key [bool]"
 Enable this if key combinations with the Alt Key don't work for you.
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 748e48b3..2a314e74 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -689,6 +689,18 @@ Set the title to "ranger" in the tmux program?
 
 Use the preview script defined in the setting I<preview_script>?
 
+=item vcs_aware [bool]
+
+Gather and display data about version control systems. Supported vcs: git, hg.
+
+=item vcs_backend_git, vcs_backend_hg, vcs_backend_bzr [string]
+
+Sets the state for the version control backend. The possible values are:
+    
+ disabled   don't display any information.
+ local      display only local state.
+ enabled    display both, local and remote state. May be slow for hg and bzr.
+
 =item xterm_alt_key [bool]
 
 Enable this if key combinations with the Alt Key don't work for you.
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index 191364b9..767d370d 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -94,6 +94,13 @@ class Default(ColorScheme):
                     fg = red
             if context.loaded:
                 bg = self.progress_bar_color
+            if context.vcsinfo:
+                fg = blue
+                attr &= ~bold
+            if context.vcscommit:
+                fg = yellow
+                attr &= ~bold
+
 
         if context.text:
             if context.highlight:
@@ -112,4 +119,33 @@ class Default(ColorScheme):
                 else:
                     bg = self.progress_bar_color
 
+
+        if context.vcsfile and not context.selected:
+            attr &= ~bold
+            if context.vcsconflict:
+                fg = magenta
+            elif context.vcschanged:
+                fg = red
+            elif context.vcsunknown:
+                fg = red
+            elif context.vcsstaged:
+                fg = green
+            elif context.vcssync:
+                fg = green
+            elif context.vcsignored:
+                fg = default
+
+        elif context.vcsremote and not context.selected:
+            attr &= ~bold
+            if context.vcssync:
+                fg = green
+            elif context.vcsbehind:
+                fg = red
+            elif context.vcsahead:
+                fg = blue
+            elif context.vcsdiverged:
+                fg = magenta
+            elif context.vcsunknown:
+                fg = red
+
         return fg, bg, attr
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 280c0cd8..594682b8 100644
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -1122,3 +1122,102 @@ class grep(Command):
             action.extend(['-e', self.rest(1), '-r'])
             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
+    """
+    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()
+
+
+class unstage(Command):
+    """
+    :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()
+
+
+class diff(Command):
+    """
+    :diff
+
+    Displays a diff of selected files against last last commited version
+    """
+    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])
+        else:
+            raise Exception("diff is empty")
+
+
+class log(Command):
+    """
+    :log
+
+    Displays the log of the current repo or files
+    """
+    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])
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index bfb2d457..1343e07e 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -41,6 +41,16 @@ set preview_script ~/.config/ranger/scope.sh
 # Use the external preview script or display simple plain text previews?
 set use_preview_script true
 
+# Be aware of version control systems and display information.
+set vcs_aware false
+
+# State of the three backends git, hg, bzr. The possible states are
+# disabled, local (only show local info), enabled (show local and remote
+# information).
+set vcs_backend_git enabled
+set vcs_backend_hg disabled
+set vcs_backend_bzr disabled
+
 # Preview images in full color with the external command "w3mimgpreview"?
 # This requires the console web browser "w3m" and a supported terminal.
 # It has been successfully tested with "xterm" and "urxvt" without tmux.
diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py
index fd669fea..8fa27a43 100644
--- a/ranger/container/settingobject.py
+++ b/ranger/container/settingobject.py
@@ -45,6 +45,10 @@ ALLOWED_SETTINGS = {
     'update_title': bool,
     'update_tmux_title': bool,
     'use_preview_script': bool,
+    'vcs_aware': bool,
+    'vcs_backend_git': str,
+    'vcs_backend_hg': str,
+    'vcs_backend_bzr': str,
     'xterm_alt_key': bool,
 }
 
diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py
new file mode 100644
index 00000000..bb696178
--- /dev/null
+++ b/ranger/ext/vcs/__init__.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# vcs - a python module to handle various version control systems
+# Copyright 2011 Abdó Roig-Maranges <abdo.roig@gmail.com>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from .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
new file mode 100644
index 00000000..a63eea54
--- /dev/null
+++ b/ranger/ext/vcs/bzr.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# vcs - a python module to handle various version control systems
+# Copyright 2012 Abdó Roig-Maranges <abdo.roig@gmail.com>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+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'
+
+
+    def _sanitize_rev(self, rev):
+        if rev == None: return None
+        rev = rev.strip()
+        if len(rev) == 0: return None
+
+        return rev
+
+
+    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]
+
+        if filelist: args = args + filelist
+
+        raw = self._bzr(self.path, args, catchout=True, silent=True)
+        L = re.findall('-+$(.*?)^-', raw + '\n---', 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)
+        return log
+
+
+    def _hg_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()
+
+
+
+    # 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)
+
+
+
+    # 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."""
+        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[p.strip()] = sta
+        return ret
+
+
+    def get_ignore_allfiles(self):
+        """Returns a set of all the ignored files in the repo"""
+        raw = self._bzr(self.path, ['ls', '--ignored'], catchout=True)
+        return set(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 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
+
+
+    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)
+
+
+    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)
+
+
+    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)
+        try:
+            L = self._log(refspec=rev)
+        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)
+        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
diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py
new file mode 100644
index 00000000..b2e79a06
--- /dev/null
+++ b/ranger/ext/vcs/git.py
@@ -0,0 +1,315 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# vcs - a python module to handle various version control systems
+# Copyright 2011, 2012 Abdó Roig-Maranges <abdo.roig@gmail.com>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import shutil
+from datetime import datetime
+
+from .vcs import Vcs, VcsError
+
+
+class Git(Vcs):
+    vcsname  = 'git'
+
+    # 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 _has_head(self):
+        """Checks whether repo has head"""
+        try:
+            self._git(self.path, ['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
+
+
+    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()
+
+
+    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)
+
+        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)
+        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()
+
+
+
+    # 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)
+
+
+
+    # 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."""
+        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: ret[m.group(2).strip()] = sta
+                else: ret[p.strip()] = sta
+            else:
+                ret[p.strip()] = sta
+        return ret
+
+
+    def get_ignore_allfiles(self):
+        """Returns a set of all the ignored files in the repo"""
+        raw = self._git(self.path, ['ls-files', '--others', '-i', '--exclude-standard'], catchout=True)
+        return set(raw.split('\n'))
+
+
+    def get_remote_status(self):
+        """Checks the status of the repo regarding sync state with remote branch"""
+        try:
+            head = self._head_ref()
+            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"
+
+
+    def get_branch(self):
+        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
+        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')
+        else:
+            return []
+
+# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py
new file mode 100644
index 00000000..08e5753b
--- /dev/null
+++ b/ranger/ext/vcs/hg.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# vcs - a python module to handle various version control systems
+# Copyright 2011, 2012 Abdó Roig-Maranges <abdo.roig@gmail.com>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import shutil
+from datetime import datetime
+try:
+    from configparser import RawConfigParser
+except ImportError:
+    from ConfigParser import RawConfigParser
+
+from .vcs import Vcs, VcsError
+
+
+class Hg(Vcs):
+    vcsname  = 'hg'
+    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
+
+        return rev
+
+
+    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]
+
+        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)
+
+        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)
+        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."""
+        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[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
+        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)
+        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')
+        else:
+            return []
+
+
+# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
new file mode 100644
index 00000000..e0d434e0
--- /dev/null
+++ b/ranger/ext/vcs/vcs.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# vcs - a python module to handle various version control systems
+# Copyright 2011, 2012 Abdó Roig-Maranges <abdo.roig@gmail.com>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import subprocess
+from datetime import datetime
+
+
+class VcsError(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:
+
+         * do NOT implement __init__. Vcs takes care of this.
+
+         * do not create change internal state. All internal state should be
+           handled in 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),
+    # 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
+        self.repo_types  = {'git': Git, 'hg': Hg, 'bzr': Bzr}
+
+        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
+            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
+        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]
+                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
+
+
+    # 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"""
+        raise NotImplementedError
+
+
+    def reset(self, filelist):
+        """Removes files from the index"""
+        raise NotImplementedError
+
+
+    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)
+
+        if os.path.commonprefix([self.root, path]) == self.root:
+            prel = os.path.relpath(path, self.root)
+            if prel in self.ignored:   return "ignored"
+            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))]) | set(['sync'])
+                for st in self.FILE_STATUS:
+                    if st in sts: return st
+            else:
+                if prel in self.status:  return self.status[prel]
+                else:                    return "sync"
+        else:
+            return "none"
+
+
+    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 {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."""
+        raise NotImplementedError
+
+
+    def get_ignore_allfiles(self):
+        """Returns a set of all the ignored files in the repo"""
+        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/fsobject/directory.py b/ranger/fsobject/directory.py
index 43fdff65..c7e58a54 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -16,6 +16,7 @@ from ranger.core.shared import SettingsAware
 from ranger.ext.accumulator import Accumulator
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.human_readable import human_readable
+from ranger.ext.vcs import VcsError
 from ranger.container.settingobject import LocalSettingObject
 
 def sort_by_basename(path):
@@ -70,6 +71,8 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
     content_outdated = False
     content_loaded = False
 
+    has_vcschild=False
+
     _cumulative_size_calculated = False
 
     sort_dict = {
@@ -223,6 +226,10 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 
                 files = []
                 disk_usage = 0
+
+                self.has_vcschild = False
+                self.load_vcs()
+
                 for name in filenames:
                     try:
                         file_lstat = os_lstat(name)
@@ -247,10 +254,42 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
                         item = File(name, preload=stats, path_is_abs=True)
                         item.load()
                         disk_usage += item.size
+
+                    # Load vcs data
+                    if self.settings.vcs_aware:
+                        item.load_vcs()
+                        if item.vcs:
+                            if item.vcs.vcsname == 'git':   backend_state = self.settings.vcs_backend_git
+                            elif item.vcs.vcsname == 'hg':  backend_state = self.settings.vcs_backend_hg
+                            elif item.vcs.vcsname == 'bzr': backend_state = self.settings.vcs_backend_bzr
+                            else:                           backend_state = 'disabled'
+
+                            if backend_state in set(['enabled', 'local']):
+                                self.has_vcschild = True
+                                try:
+                                    if self.vcs_outdated or item.vcs_outdated:
+                                        item.vcs_outdated = False
+                                        item.vcs.get_status()  # caches the file status for get_file_status()
+                                        item.vcsbranch = item.vcs.get_branch()
+                                        item.vcshead = item.vcs.get_info(item.vcs.HEAD)
+                                        if item.path == item.vcs.root and backend_state == 'enabled':
+                                            item.vcsremotestatus = item.vcs.get_remote_status()
+                                    else:
+                                        item.vcsbranch = self.vcsbranch
+                                        item.vcshead = self.vcshead
+                                    item.vcsfilestatus = item.vcs.get_file_status(item.path)
+                                except VcsError as err:
+                                    item.vcsbranch = None
+                                    item.vcshead = None
+                                    item.vcsremotestatus = 'unknown'
+                                    item.vcsfilestatus = 'unknown'
+                                    self.fm.notify("Can not load vcs data on %s: %s" % (item.path, err), bad=True)
+
                     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 = files
diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py
index 16b71945..0b4f9776 100644
--- a/ranger/fsobject/file.py
+++ b/ranger/fsobject/file.py
@@ -3,6 +3,7 @@
 
 import re
 from ranger.fsobject import FileSystemObject
+from ranger.core.shared import SettingsAware
 
 N_FIRST_BYTES = 256
 control_characters = set(chr(n) for n in
@@ -37,7 +38,7 @@ PREVIEW_WHITELIST = re.compile(r"""
         $
 """, re.VERBOSE | re.IGNORECASE)
 
-class File(FileSystemObject):
+class File(FileSystemObject, SettingsAware):
     is_file = True
     preview_data = None
     preview_known = False
diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py
index b39ea40b..506f13d9 100644
--- a/ranger/fsobject/fsobject.py
+++ b/ranger/fsobject/fsobject.py
@@ -18,6 +18,7 @@ from ranger.ext.shell_escape import shell_escape
 from ranger.ext.spawn import spawn
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.human_readable import human_readable
+from ranger.ext.vcs import Vcs
 
 if hasattr(str, 'maketrans'):
     maketrans = str.maketrans
@@ -67,6 +68,13 @@ class FileSystemObject(FileManagerAware):
 
     size = 0
 
+    (vcs,
+     vcsfilestatus,
+     vcsremotestatus,
+     vcsbranch,
+     vcshead) = (None,) * 5
+
+    vcs_outdated = False
 
     def __init__(self, path, preload=None, path_is_abs=False):
         if not path_is_abs:
@@ -185,6 +193,38 @@ class FileSystemObject(FileManagerAware):
                 return None  # it is impossible to get the link destination
         return self.path
 
+    def load_vcs(self):
+        """
+        Reads data regarding the version control system the object is on.
+        Does not load content specific data.
+        """
+
+        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()
+            self.vcs = rootdir.vcs
+            if rootdir.vcs_outdated:
+                self.vcs_outdated = True
+
     def load(self):
         """Loads information about the directory itself.
 
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index 82bf5f60..ecef7d07 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -15,7 +15,11 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo',
         'help_markup', # COMPAT
         'seperator', 'key', 'special', 'border', # COMPAT
         'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded',
-        'keybuffer']
+        'keybuffer',
+                'infostring',
+                'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit',
+                'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored',
+                'vcsstaged', 'vcssync', 'vcsbehind', 'vcsahead', 'vcsdiverged']
 
 class Context(object):
     def __init__(self, keys):
diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py
index 617663a6..f2e52c0a 100644
--- a/ranger/gui/widgets/__init__.py
+++ b/ranger/gui/widgets/__init__.py
@@ -1,4 +1,23 @@
+# -*- coding: utf-8 -*-
+
 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"])}
+
+    vcsremotestatus_symb = {'none':     (' ',  []),
+                'sync':     ('=', ["vcssync"]),
+                'behind':   ('<', ["vcsbehind"]),
+                'ahead':    ('>', ["vcsahead"]),
+                'diverged': ('Y', ["vcsdiverged"]),
+                'unknown':  ('?', ["vcsunknown"])}
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 34806b7f..afe8e13b 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- coding: utf-8 -*-
 # Copyright (C) 2009-2013  Roman Zimbelmann <hut@lavabit.com>
 # This software is distributed under the terms of the GNU GPL version 3.
 
@@ -13,9 +13,12 @@ from .pager import Pager
 from ranger.fsobject import BAD_INFO
 from ranger.ext.widestring import WideString
 
+from ranger.gui.color import *
+
 class BrowserColumn(Pager):
     main_column = False
     display_infostring = False
+    display_vcsstate   = True
     scroll_begin = 0
     target = None
     last_redraw_time = -1
@@ -92,7 +95,7 @@ class BrowserColumn(Pager):
     def execute_curses_batch(self, line, commands):
         """Executes a list of "commands" which can be easily cached.
 
-        "commands" is a list of lists.  Each element contains
+        "commands" is a list of lists.    Each element contains
         a text and an attribute.  First, the attribute will be
         set with attrset, then the text is printed.
 
@@ -137,7 +140,7 @@ class BrowserColumn(Pager):
             self.need_redraw = True
             self.old_dir = self.target
 
-        if self.target:  # don't garbage collect this directory please
+        if self.target:     # don't garbage collect this directory please
             self.target.use()
 
         if self.target and self.target.is_directory \
@@ -225,7 +228,6 @@ class BrowserColumn(Pager):
         self._set_scroll_begin()
 
         copied = [f.path for f in self.fm.copy_buffer]
-        ellipsis = self.ellipsis[self.settings.unicode_ellipsis]
 
         selected_i = self.target.pointer
         for line in range(self.hei):
@@ -246,100 +248,142 @@ class BrowserColumn(Pager):
 
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                     drawn.path in copied, tagged_marker, drawn.infostring,
-                    self.fm.do_cut)
+                    drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
                 self.color_reset()
                 continue
 
+            text = drawn.basename
+            if drawn.marked and (self.main_column or self.settings.display_tags_in_all_columns):
+                text = " " + text
+
+            # Computing predisplay data. predisplay contains a list of lists [string, colorlst]
+            # where string is a piece of string to display, and colorlst a list of contexts
+            # that we later pass to the colorscheme, to compute the curses attribute.
+            predisplay_left = []
+            predisplay_right = []
+
+            predisplay_left     = predisplay_left + self._draw_tagged_display(tagged, tagged_marker)
+            predisplay_right = predisplay_right + self._draw_vcsstring_display(drawn)
+            space = self.wid - self._total_len(predisplay_left) - self._total_len(predisplay_right)
+
+            # If not enough space
+            if space <= 2:
+                predisplay_right = []
+                predisplay_left     = []
+
+            predisplay_left = predisplay_left + self._draw_text_display(text, space)
+            space = self.wid - self._total_len(predisplay_left)  - self._total_len(predisplay_right)
+
+            predisplay_right = self._draw_infostring_display(drawn, space) + predisplay_right
+            space = self.wid - self._total_len(predisplay_left)  - self._total_len(predisplay_right)
+
+            if space > 0:
+                predisplay_left.append([' ' * space, []])
+            elif space < 0:
+                raise Exception("Error: there is not enough space to write the text. I have computed spaces wrong.")
+
+            # Computing display data. Now we compute the display_data list ready to display in
+            # curses. It is a list of lists [string, attr]
+
+            this_color = base_color + list(drawn.mimetype_tuple) + self._draw_directory_color(i, drawn, copied)
             display_data = []
             drawn.display_data[key] = display_data
 
-            if self.display_infostring and drawn.infostring \
-                    and self.settings.display_size_in_main_column:
-                infostring = str(drawn.infostring) + " "
-            else:
-                infostring = ""
+            predisplay = predisplay_left + predisplay_right
+            for txt, color in predisplay:
+                attr = self.settings.colorscheme.get_attr(*(this_color + color))
+                display_data.append([txt, attr])
 
-            this_color = base_color + list(drawn.mimetype_tuple)
-            text = drawn.basename
+            self.execute_curses_batch(line, display_data)
+            self.color_reset()
 
-            space = self.wid - len(infostring)
-            if self.main_column:
-                space -= 2
-            elif self.settings.display_tags_in_all_columns:
-                space -= 1
+    def _total_len(self, predisplay):
+        return sum([len(WideString(s)) for s, L in predisplay])
 
-            if i == selected_i:
-                this_color.append('selected')
+    def _draw_text_display(self, text, space):
+        wtext = WideString(text)
+        if len(wtext) > space:
+            wtext = wtext[:max(0, space - 1)] + self.ellipsis[self.settings.unicode_ellipsis]
 
-            if drawn.marked:
-                this_color.append('marked')
-                if self.main_column or self.settings.display_tags_in_all_columns:
-                    text = " " + text
+        return [[str(wtext), []]]
 
+    def _draw_tagged_display(self, tagged, tagged_marker):
+        tagged_display = []
+        if (self.main_column or self.settings.display_tags_in_all_columns) and self.wid > 2:
             if tagged:
-                this_color.append('tagged')
-
-            if drawn.is_directory:
-                this_color.append('directory')
+                tagged_display.append([tagged_marker, ['tag_marker']])
             else:
-                this_color.append('file')
-
-            if drawn.stat:
-                mode = drawn.stat.st_mode
-                if mode & stat.S_IXUSR:
-                    this_color.append('executable')
-                if stat.S_ISFIFO(mode):
-                    this_color.append('fifo')
-                if stat.S_ISSOCK(mode):
-                    this_color.append('socket')
-                if drawn.is_device:
-                    this_color.append('device')
-
-            if drawn.path in copied:
-                this_color.append('cut' if self.fm.do_cut else 'copied')
-
-            if drawn.is_link:
-                this_color.append('link')
-                this_color.append(drawn.exists and 'good' or 'bad')
-
-            attr = self.settings.colorscheme.get_attr(*this_color)
-
-            if (self.main_column or self.settings.display_tags_in_all_columns) \
-                    and tagged and self.wid > 2:
-                this_color.append('tag_marker')
-                tag_attr = self.settings.colorscheme.get_attr(*this_color)
-                display_data.append([tagged_marker, tag_attr])
+                tagged_display.append([" ", ['tag_marker']])
+        return tagged_display
+
+    def _draw_infostring_display(self, drawn, space):
+        infostring_display = []
+        if self.display_infostring and drawn.infostring \
+                and self.settings.display_size_in_main_column:
+            infostring = str(drawn.infostring) + " "
+            if len(infostring) <= space:
+                infostring_display.append([infostring, ['infostring']])
+        return infostring_display
+
+    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:
-                text = " " + text
-                space += 1
-
-            wtext = WideString(text)
-            if len(wtext) > space:
-                wtext = wtext[:max(0, space - 1)] + ellipsis
-            text = str(wtext)
-
-            display_data.append([text, attr])
-
-            padding = self.wid - len(wtext)
-            if tagged and (self.main_column or \
-                    self.settings.display_tags_in_all_columns):
-                padding -= 1
-            if infostring:
-                if len(wtext) + 1 + len(infostring) > self.wid:
-                    pass
-                else:
-                    padding -= len(infostring)
-                    padding = max(0, padding)
-                    infostring = (" " * padding) + infostring
-                    display_data.append([infostring, attr])
+                vcsstr = " "
+                vcscol = []
+            vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
+
+            if drawn.vcsremotestatus:
+                vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcsremotestatus]
             else:
-                display_data.append([" " * max(0, padding), attr])
 
-            self.execute_curses_batch(line, display_data)
-            self.color_reset()
+                vcsstr = " "
+                vcscol = []
+            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):
+        this_color = []
+        if i == self.target.pointer:
+            this_color.append('selected')
+
+        if drawn.marked:
+            this_color.append('marked')
+
+        if self.fm.tags and drawn.realpath in self.fm.tags:
+            this_color.append('tagged')
+
+        if drawn.is_directory:
+            this_color.append('directory')
+        else:
+            this_color.append('file')
+
+        if drawn.stat:
+            mode = drawn.stat.st_mode
+            if mode & stat.S_IXUSR:
+                this_color.append('executable')
+            if stat.S_ISFIFO(mode):
+                this_color.append('fifo')
+            if stat.S_ISSOCK(mode):
+                this_color.append('socket')
+            if drawn.is_device:
+                this_color.append('device')
+
+        if drawn.path in copied:
+            this_color.append('cut' if self.fm.do_cut else 'copied')
+
+        if drawn.is_link:
+            this_color.append('link')
+            this_color.append(drawn.exists and 'good' or 'bad')
+
+        return this_color
 
     def _get_scroll_begin(self):
         """Determines scroll_begin (the position of the first displayed file)"""
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 1ce7af11..e16b6274 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -181,6 +181,26 @@ 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)
+            else:
+                vcsinfo = '(%s)' % (target.vcs.vcsname)
+
+            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(vcsstr.strip(), 'vcsremote', *vcscol)
+            if target.vcshead:
+                left.add_space()
+                left.add('%s' % target.vcshead['summary'], 'vcscommit')
+
     def _get_owner(self, target):
         uid = target.stat.st_uid
 
@@ -205,6 +225,8 @@ class StatusBar(Widget):
             except KeyError:
                 return str(gid)
 
+
+
     def _get_right_part(self, bar):
         right = bar.right
         if self.column is None:
@@ -261,7 +283,7 @@ class StatusBar(Widget):
                 right.add('{0:0>.0f}%'.format(100.0 * pos / max_pos),
                         base, 'percentage')
         else:
-            right.add('0/0  All', base, 'all')
+            right.add('0/0    All', base, 'all')
 
     def _print_result(self, result):
         self.win.move(0, 0)
diff --git a/setup.py b/setup.py
index 72f5db00..5d71cb53 100755
--- a/setup.py
+++ b/setup.py
@@ -28,4 +28,5 @@ if __name__ == '__main__':
                   'ranger.ext',
                   'ranger.fsobject',
                   'ranger.gui',
-                  'ranger.gui.widgets'))
+                  'ranger.gui.widgets',
+                  'ranger.ext.vcs'))