summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorRyan Burns <rdburns@gmail.com>2015-08-16 21:16:22 -0400
committerRyan Burns <rdburns@gmail.com>2015-08-16 21:16:22 -0400
commitd734fc8ea38cbac13a9fbd1807a9552495734e3b (patch)
tree182abde329aa46df7c9a85b7836c686a968a3f53
parent2344714fcbaa3697f77dd0cb4477a8f8019c7dc4 (diff)
downloadranger-d734fc8ea38cbac13a9fbd1807a9552495734e3b.tar.gz
Implemented Subversion VCS support.
-rw-r--r--ranger/container/fsobject.py2
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/ext/vcs/svn.py279
-rw-r--r--ranger/ext/vcs/vcs.py3
4 files changed, 284 insertions, 1 deletions
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index a8c70e03..334cc2f9 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -281,6 +281,8 @@ class FileSystemObject(FileManagerAware, SettingsAware):
                 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'
 
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index 1d91495f..db8ca45c 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -60,6 +60,7 @@ ALLOWED_SETTINGS = {
     'vcs_backend_bzr': str,
     'vcs_backend_git': str,
     'vcs_backend_hg': str,
+    'vcs_backend_svn': str,
     'xterm_alt_key': bool,
 }
 
diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py
new file mode 100644
index 00000000..f673d66b
--- /dev/null
+++ b/ranger/ext/vcs/svn.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+# Author: Abdó Roig-Maranges <abdo.roig@gmail.com>, 2011-2012
+#         Ryan Burns <rdburns@gmail.com>, 2015
+#
+# R. Burns start with Abdó's Hg module and modified it for Subversion.
+#
+# vcs - a python module to handle various version control systems
+
+from __future__ import with_statement
+import os
+import re
+import shutil
+import logging
+#logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG,
+#                    filemode='w')
+from datetime import datetime
+try:
+    from configparser import RawConfigParser
+except ImportError:
+    from ConfigParser import RawConfigParser
+
+from .vcs import Vcs, VcsError
+
+
+class SVN(Vcs):
+    vcsname  = 'svn'
+    HEAD = 'HEAD'
+    # Auxiliar stuff
+    #---------------------------
+
+    def _svn(self, path, args, silent=True, catchout=False, bytes=False):
+        return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, bytes=bytes)
+
+
+    def _has_head(self):
+        """Checks whether repo has head"""
+        return True
+
+
+    def _sanitize_rev(self, rev):
+        if rev == None: return None
+        rev = rev.strip()
+        if len(rev) == 0: return None
+        if rev[-1] == '+': rev = rev[:-1]
+
+        try:
+            if int(rev) == 0: return None
+        except:
+            pass
+
+        return rev
+
+
+    def _log(self, refspec=None, maxres=None, filelist=None):
+        """ Retrieves log message and parses it.
+        """
+        args = ['log']
+
+        if refspec:  args = args + ['--limit', '1', '-r', refspec]
+        elif maxres: args = args + ['--limit', str(maxres)]
+
+        if filelist: args = args + filelist
+        logging.debug('Running svn log')
+        logging.debug(args)
+
+        raw = self._svn(self.path, args, catchout=True)
+        logging.debug(raw)
+        L = re.findall("""^[-]*\s*             # Dash line
+                       r([0-9]*)\s\|\s         # Revision          [0]
+                       (.*)\s\|\s              # Author            [1]
+                       (.*?)\s                 # Date              [2]
+                       (.*?)\s                 # Time              [3]
+                       .*?\|\s*?               # Dump rest of date string
+                       ([0-9])+\sline(?:s)?\s* # Number of line(s) [4]
+                       (.*)\s                  # Log Message       [5]
+                       [-]+\s*                 # Dash line
+                       $""", raw, re.MULTILINE | re.VERBOSE)
+
+        log = []
+        for t in L:
+            logging.debug(t)
+            dt = {}
+            dt['short'] = t[0].strip()
+            dt['revid'] = t[0].strip()
+            dt['author'] = t[1].strip()
+            dt['date'] = datetime.strptime( t[2]+'T'+t[3], "%Y-%m-%dT%H:%M:%S" )
+            dt['summary'] = t[5].strip()
+            log.append(dt)
+            logging.debug(log)
+        return log
+
+
+    def _svn_file_status(self, st):
+        if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st)
+        if   st in "A":      return 'staged'
+        elif st in "D":      return 'deleted'
+        elif st in "I":      return 'ignored'
+        elif st in "?":      return 'untracked'
+        elif st in "C":      return 'conflict'
+        else:                return 'unknown'
+
+
+
+    # Repo creation
+    #---------------------------
+
+    def init(self):
+        """Initializes a repo in current path"""
+        self._svn(self.path, ['init'])
+        self.update()
+
+
+    def clone(self, src):
+        """Checks out SVN repo"""
+        name = os.path.basename(self.path)
+        path = os.path.dirname(self.path)
+        try:
+            os.rmdir(self.path)
+        except OSError:
+            raise VcsError("Can't clone to %s. It is not an empty directory" % self.path)
+
+        self._svn(path, ['co', src, name])
+        self.update()
+
+
+
+    # Action Interface
+    #---------------------------
+
+    def commit(self, message):
+        """Commits with a given message"""
+        self._svn(self.path, ['commit', '-m', message])
+
+
+    def add(self, filelist=None):
+        """Adds files to the index, preparing for commit"""
+        if filelist != None: self._svn(self.path, ['add'] + filelist)
+        else:                self._svn(self.path, ['add'])
+
+
+    def reset(self, filelist=None):
+        """Equivalent to svn revert"""
+        if filelist == None: filelist = self.get_status_allfiles().keys()
+        self._svn(self.path, ['revert'] + filelist)
+
+
+    def pull(self):
+        """Executes SVN Update"""
+        self._svn(self.path, ['update'])
+
+
+    def push(self):
+        """Push doesn't have an SVN analog."""
+        raise NotImplementedError
+
+
+    def checkout(self, rev):
+        """Checks out a branch or revision"""
+        raise NotImplementedError
+        self._svn(self.path, ['update', rev])
+
+
+    def extract_file(self, rev, name, dest):
+        """Extracts a file from a given revision and stores it in dest dir"""
+        if rev == self.INDEX:
+            shutil.copyfile(os.path.join(self.path, name), dest)
+        else:
+            file_contents = self._svn(self.path, ['cat', '-r', rev, name], catchout=True)
+            with open(dest, 'w') as f:
+                f.write(file_contents)
+
+
+    # Data Interface
+    #---------------------------
+
+    def get_status_allfiles(self):
+        """Returns a dict indexed by files not in sync their status as values.
+           Paths are given relative to the root. Strips trailing '/' from dirs."""
+        raw = self._svn(self.path, ['status'], catchout=True, bytes=True)
+#        logging.debug(raw)
+        L = re.findall('^(.)\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._svn_file_status(st)
+            ret[os.path.normpath(p.strip())] = sta
+        return ret
+
+
+    def get_ignore_allfiles(self):
+        """Returns a set of all the ignored files in the repo"""
+        raw = self._svn(self.path, ['status'], catchout=True, bytes=True)
+#        logging.debug(raw)
+        L = re.findall('^I\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE)
+        return set(L)
+
+
+    def get_remote_status(self):
+        """Checks the status of the repo regarding sync state with remote branch.
+
+        I'm not sure this make sense for SVN so we're just going to return 'sync'"""
+        return 'sync'
+
+    def get_branch(self):
+        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
+        return None
+        branch = self._svn(self.path, ['branch'], catchout=True)
+        return branch or None
+
+
+    def 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._svn(self.path, args, catchout=True)
+
+
+    def get_raw_diff(self, refspec=None, filelist=None):
+        """Gets the raw diff as a string"""
+        args = ['diff', '--git']
+        if refspec:  args = args + [refspec]
+        if filelist: args = args + filelist
+        return self._svn(self.path, args, catchout=True)
+
+
+    def get_remote(self, rev=None):
+        """Returns the url for the remote repo attached to head"""
+        raw = self.get_info(rev=rev)
+        L = re.findall('URL: (.*)\n', raw.decode('utf-8'))
+
+        return L[0][0]
+
+
+    def get_revision_id(self, rev=None):
+        """Get a canonical key for the revision rev.
+
+        This is just returning the rev for SVN"""
+        if rev == None: rev = self.HEAD
+        elif rev == self.INDEX: return None
+        rev = self._sanitize_rev(rev)
+        return rev
+
+
+    def get_info(self, rev=None):
+        """Gets info about the given revision rev"""
+        if rev == None: rev = self.HEAD
+        rev = self._sanitize_rev(rev)
+        if rev == self.HEAD and not self._has_head(): return None
+        logging.debug('refspec is ' + str(rev))
+        L = self._log(refspec=rev)
+        logging.debug(len(L))
+        if len(L) == 0:
+            raise VcsError("Revision %s does not exist" % rev)
+        elif len(L) > 1:
+            raise VcsError("More than one instance of revision %s ?!?" % rev)
+        else:
+            return L[0]
+
+
+    def get_files(self, rev=None):
+        """Gets a list of files in revision rev"""
+        if rev == None: rev = self.HEAD
+        rev = self._sanitize_rev(rev)
+
+        raw = self._svn(self.path, ['ls', "-r", rev], catchout=True)
+        return raw.split('\n')
+
+
+# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index 9bbb1779..3f7236f5 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -51,7 +51,8 @@ class Vcs(object):
         from .git import Git
         from .hg  import Hg
         from .bzr import Bzr
-        self.repo_types  = {'git': Git, 'hg': Hg, 'bzr': Bzr}
+        from .svn import SVN
+        self.repo_types  = {'git': Git, 'hg': Hg, 'bzr': Bzr, 'svn': SVN}
 
         self.path = os.path.expanduser(path)
         self.status = {}