Switch to side-by-side view

--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -1,266 +1,251 @@
 import os
-import errno
-import stat
+import shutil
+import string
 import logging
 import subprocess
-import cPickle as pickle
+from hashlib import sha1
 from cStringIO import StringIO
 from datetime import datetime
 
+import tg
 import pysvn
-import pymongo
 from pylons import c
 
-from ming.orm.mapped_class import MappedClass
-from ming.orm.property import FieldProperty
+from ming.base import Object
+from ming.orm import MappedClass, FieldProperty, session
 from ming.utils import LazyProperty
 
 from allura import model as M
+from allura.model.repository import GitLikeTree
 
 log = logging.getLogger(__name__)
 
-def on_import():
-    SVNRepository.CommitClass = SVNCommit
-    SVNCommit.TreeClass = SVNTree
-    SVNTree.BlobClass = SVNBlob
-
-class SVNRepository(M.Repository):
-    MAGIC_FILENAME='.SOURCEFORGE-REPOSITORY'
+class Repository(M.Repository):
+    tool_name='SVN'
+    repo_id='svn'
+    type_s='SVN Repository'
     class __mongometa__:
         name='svn-repository'
-
-    def index(self):
-        result = super(SVNRepository, self).index()
-        result.update(
-            type_s='SVNRepository')
-        return result
+    branches = FieldProperty([dict(name=str,object_id=str)])
+
+    def __init__(self, **kw):
+        super(Repository, self).__init__(**kw)
+        self._impl = SVNImplementation(self)
+
+    def readonly_clone_command(self):
+        return 'svn checkout svn://%s' % self.scm_url_path
+
+    def readwrite_clone_command(self):
+        return 'svn checkout svn+ssh://%s@%s' % (c.user.username, self.scm_url_path)
+
+    def _log(self, rev, skip, max_count):
+        ci = self.commit(rev)
+        if ci is None: return []
+        return ci.log(int(skip), int(max_count))
+
+    def count(self, *args, **kwargs):
+        return super(Repository, self).count(None)
+
+    def log(self, branch=None, offset=0, limit=10):
+        return list(self._log(rev=branch, skip=offset, max_count=limit))
+
+    def latest(self, branch=None):
+        if self._impl is None: return None
+        if not self.heads: return None
+        return self._impl.commit(self.heads[0].object_id)
+
+class SVNImplementation(M.RepositoryImplementation):
+    post_receive_template = string.Template(
+        '#!/bin/bash\n'
+        'curl -s $url\n')
+
+    def __init__(self, repo):
+        self._repo = repo
 
     @LazyProperty
-    def _impl(self):
+    def _svn(self):
         return pysvn.Client()
 
     @LazyProperty
-    def local_url(self):
-        return 'file://%s/%s' % (self.fs_path, self.name)
-
-    @LazyProperty
-    def last_revision(self):
-        info = self._impl.info2(
-            self.local_url,
-            revision=pysvn.Revision(pysvn.opt_revision_kind.head),
-            recurse=False)
-        return info[0][1].rev.number
+    def _url(self):
+        return 'file://%s/%s' % (self._repo.fs_path, self._repo.name)
+
+    def shorthand_for_commit(self, commit):
+        return '[r%d]' % self._revno(commit.object_id)
+
+    def url_for_commit(self, commit):
+        return '%s%d/' % (
+            self._repo.url(), self._revno(commit.object_id))
 
     def init(self):
-        if not self.fs_path.endswith('/'): self.fs_path += '/'
-        try:
-            os.makedirs(self.fs_path)
-        except OSError, e: # pragma no cover
-            if e.errno != errno.EEXIST: raise
-        # We may eventually require --template=...
-        log.info('svnadmin create %s%s', self.fs_path, self.name)
-        result = subprocess.call(['svnadmin', 'create', self.name],
+        fullname = self._setup_paths()
+        log.info('svn init %s', fullname)
+        if os.path.exists(fullname):
+            shutil.rmtree(fullname)
+        subprocess.call(['svnadmin', 'create', self._repo.name],
                                  stdin=subprocess.PIPE,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
-                                 cwd=self.fs_path)
-        magic_file = os.path.join(self.fs_path, self.name, self.MAGIC_FILENAME)
-        with open(magic_file, 'w') as f:
-            f.write('svn')
-        os.chmod(magic_file, stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
-        self.status = 'ready'
-
-    def _log(self, url, **kwargs):
-        try:
-            offset = kwargs.pop('offset', 0)
-            limit=kwargs.get('limit', 10)
-            if offset != 0:
-                latest = self._impl.log(url, limit=1)
-                if not latest: return []
-                latest = latest[0]
-                revno = latest.revision.number-offset
-                limit = min(limit, revno)
-                if limit <= 0: return []
-                revno = max(revno, 0)
-                kwargs['revision_start'] = pysvn.Revision(
-                        pysvn.opt_revision_kind.number,
-                        revno)
-            commits = self._impl.log(url, **kwargs)
-            return [ self.CommitClass.from_repo_object(entry, self) for entry in commits ]
-        except pysvn.ClientError: # pragma no cover
-            # probably an empty repo
-            return []
-        except:  # pragma no cover
-            log.exception('Error performing SVN log:')
-            return []
-
-    def log(self, branch=None, offset=0, limit=10):
-        return self._log(self.local_url, offset=offset, limit=limit)
-
-    def count(self, branch=None):
-        try:
-            latest = self._impl.log(self.local_url, limit=1)
-        except pysvn.ClientError:
-            return 0
-        if not latest: return 0
-        return latest[0].revision.number
-
-    @LazyProperty
-    def latest(self):
-        try:
-            l = self._impl.log(self.local_url, limit=1)
-        except pysvn.ClientError:
-            return None
-        if l:
-            return self.CommitClass.from_repo_object(l[0], self)
-        else:
-            return None
-
-    def commit(self, revision):
-        try:
-            r = self._impl.log(
-                self.local_url,
-                revision_start=pysvn.Revision(
-                    pysvn.opt_revision_kind.number, revision),
-                limit=1)
-        except pysvn.ClientError:
-            return None
-        if r: return self.CommitClass.from_repo_object(r[0], self)
-        else: return None
-
-    # def diff(self, r0, r1):
-    #     r0 = pysvn.Revision(pysvn.opt_revision_kind.number, r0)
-    #     r1 = pysvn.Revision(pysvn.opt_revision_kind.number, r1)
-    #     return self._impl.diff(
-    #         '/tmp', self.local_url, r0,
-    #         self.local_url, r1)
-
-    def diff_summarize(self, r0, r1):
-        r0 = pysvn.Revision(pysvn.opt_revision_kind.number, r0)
-        r1 = pysvn.Revision(pysvn.opt_revision_kind.number, r1)
-        result = self._impl.diff_summarize(
-            self.local_url, r0,
-            self.local_url, r1)
+                                 cwd=self._repo.fs_path)
+        self._setup_special_files()
+        self._repo.status = 'ready'
+
+    def refresh_heads(self):
+        info = self._svn.info2(
+            self._url,
+            revision=pysvn.Revision(pysvn.opt_revision_kind.head),
+            recurse=False)[0][1]
+        oid = self._oid(info.rev.number)
+        self._repo.heads = [ Object(name=None, object_id=oid) ]
+        # Branches and tags aren't really supported in subversion
+        self._repo.branches = []
+        self._repo.repo_tags = []
+        session(self._repo).flush()
+
+    def commit(self, rev):
+        if rev in ('HEAD', None):
+            if not self._repo.heads: return None
+            oid = self._repo.heads[0].object_id
+        elif isinstance(rev, int) or rev.isdigit():
+            oid = self._oid(rev)
+        else:
+            oid = rev
+        result = M.Commit.query.get(repo_id='svn', object_id=oid)
+        if result is None: return None
+        result.set_context(self._repo)
         return result
 
-class SVNCommit(M.Commit):
-    type_s='SvnCommit'
-
-    @classmethod
-    def from_repo_object(cls, entry, repo):
-        result = cls(id=entry.revision.number, repo=repo)
-        result.__dict__['_impl'] = entry
-        result.author_username=entry.get('author')
-        result.datetime=datetime.utcfromtimestamp(entry.date)
+    def new_commits(self):
+        head_revno = self._revno(self._repo.heads[0].object_id)
+        result = []
+        for revno in range(1, head_revno+1):
+            oid = self._oid(revno)
+            if M.Commit.query.find(dict(repo_id='svn', object_id=oid)).count() == 0:
+                result.append(oid)
         return result
 
-    @LazyProperty
-    def revision(self):
-        return pysvn.Revision(pysvn.opt_revision_kind.number, self._id)
-
-    def context(self):
-        prev = next = None
-        if self.revision.number < self._repo.latest.revision.number:
-            next = self._repo.commit(self.revision.number+1)
-        if self.revision.number > 1:
-            prev = self._repo.commit(self.revision.number-1)
+    def commit_context(self, commit):
+        revno = int(commit.object_id.split(':')[1])
+        prev,next=[],[]
+        if revno > 1:
+            prev = [ self.commit(revno - 1) ]
+        if revno < self._revno(self._repo.heads[0].object_id):
+            next = [ self.commit(revno + 1) ]
         return dict(prev=prev, next=next)
 
-    def __getattr__(self, name):
-        return getattr(self._impl, name)
-
-    def dump_ref(self):
-        '''Return a pickle-serializable reference to an artifact'''
-        try:
-            d = M.ArtifactReference(dict(
-                    project_id=c.project._id,
-                    mount_point=c.app.config.options.mount_point,
-                    artifact_type=pymongo.bson.Binary(pickle.dumps(self.__class__)),
-                    artifact_id=self._id))
-            return d
-        except AttributeError: # pragma no cover
-            return None
-
-    def url(self):
-        return self._repo.url() + str(self._id) + '/'
-
-    def primary(self, *args):
-        return self
-
-    def shorthand_id(self):
-        return '[r%s]' % self._id
-
-    def diff_summarize(self, other_rev=None):
-        if other_rev is None: other_rev = self._id-1
-        for s in self._repo.diff_summarize(other_rev, self._id):
-            yield str(s.summarize_kind), s.path
-
-    # def diff(self, other_rev=None):
-    #     if other_rev is None: other_rev = self._id-1
-    #     return super(SVNCommit, self).diff(other_rev)
-
-    @LazyProperty
-    def author(self):
-        return M.User.by_username(self.author_username)
-
-class SVNTree(M.Tree):
-
-    def ls(self):
-        try:
-            for dirent in self._repo._impl.ls(
-                self._repo.local_url + self.path(),
-                revision=self._commit.revision):
-                name = dirent.name.rsplit('/')[-1]
-                date = datetime.fromtimestamp(dirent.time)
-                href = name
-                if dirent.kind == pysvn.node_kind.dir:
-                    href = href + '/'
-                commit = self._repo.commit(dirent.created_rev.number)
-                yield dict(dirent, name=name, date=date, href=href,
-                           commit=commit)
-        except pysvn.ClientError:
-            pass
-
-    def is_blob(self, name):
-        dirent = self._repo._impl.ls(
-            self._repo.local_url + self.path()+name,
-            revision=self._commit.revision)
-        if len(dirent) != 1: return False
-        dirent = dirent[0]
-        if dirent.kind == pysvn.node_kind.file:
-            return True
-        return False
-
-class SVNBlob(M.Blob):
-
-    def __iter__(self):
-        fp = StringIO(self.text)
-        return iter(fp)
-
-    @LazyProperty
-    def text(self):
-        return self._repo._impl.cat(
-            self._repo.local_url + self.path(),
-            revision=self._commit.revision)
-
-    def context(self):
-        entries = self._repo._log(self._repo.local_url + self.path())
-        result = dict(prev=None, next=None)
-        path = self._tree.path().split('/')[1:-1]
-        prev=next=None
-        for ent in entries:
-            if ent.revision.number < self._commit.revision.number:
-                prev=ent
-                break
-            if ent.revision.number > self._commit.revision.number:
-                next=ent
-        if prev:
-            ci = SVNCommit.from_repo_object(prev, self._repo)
-            result['prev'] = ci.tree().get_blob(self.filename, path)
-        if next:
-            ci = SVNCommit.from_repo_object(next, self._repo)
-            result['next'] = ci.tree().get_blob(self.filename, path)
-        return result
-
-on_import()
+    def refresh_commit(self, ci, seen_object_ids):
+        log.info('Refresh %r', ci)
+        revno = self._revno(ci.object_id)
+        rev = pysvn.Revision(
+            pysvn.opt_revision_kind.number,
+            revno)
+        log_entry = self._svn.log(
+            self._url,
+            revision_start=rev,
+            limit=1,
+            discover_changed_paths=True)[0]
+        # Save commit metadata
+        ci.committed = Object(
+            name=log_entry.author,
+            email='',
+            date=datetime.fromtimestamp(log_entry.date))
+        ci.authored=Object(ci.committed)
+        ci.message=log_entry.message
+        if revno > 1:
+            parent_oid = self._oid(revno - 1)
+            parent_ci = self.commit(parent_oid)
+            ci.parent_ids = [ parent_oid ]
+        else:
+            parent_ci = None
+        # Save commit tree (must build a fake git-like tree from the log entry)
+        fake_tree = self._tree_from_log(parent_ci, log_entry)
+        ci.tree_id = fake_tree.hex()
+        tree, isnew = M.Tree.upsert('svn', fake_tree.hex())
+        if isnew:
+            tree.set_context(ci)
+            tree.set_last_commit(ci)
+            self._refresh_tree(tree, fake_tree)
+
+    def log(self, object_id, skip, count):
+        revno = self._revno(object_id)
+        result = []
+        while count and revno:
+            if skip == 0:
+                result.append(self._oid(revno))
+                count -= 1
+            else:
+                skip -= 1
+            revno -= 1
+        if revno:
+            return result, [ self._oid(revno) ]
+        else:
+            return result, []
+
+    def open_blob(self, blob):
+        data = self._svn.cat(
+            self._url + blob.path(),
+            revision=pysvn.Revision(
+                pysvn.opt_revision_kind.number,
+                self._revno(blob.commit.object_id)))
+        return StringIO(data)
+
+    def _setup_receive_hook(self):
+        'Set up the hg changegroup hook'
+        text = self.post_receive_template.substitute(
+            url=tg.config.get('base_url', 'localhost:8080')
+            + self._repo.url()[1:] + 'refresh')
+        fn = os.path.join(self._repo.fs_path, self._repo.name, 'hooks', 'post-commit')
+        with open(fn, 'wb') as fp:
+            fp.write(text)
+        os.chmod(fn, 0755)
+
+    def _tree_from_log(self, parent_ci, log_entry):
+        '''Build a fake git-like tree from a parent commit and a log entry'''
+        if parent_ci is None:
+            root = GitLikeTree()
+        else:
+            session(parent_ci).flush() # need to make sure the tree is in mongo first
+            root = GitLikeTree.from_tree(parent_ci.tree)
+        for path in log_entry.changed_paths:
+            if path.action == 'D':
+                root.del_blob(path.path)
+            else:
+                try:
+                    data = self._svn.cat(
+                        self._url + path.path,
+                        revision=log_entry.revision)
+                    oid = sha1(data).hexdigest()
+                    root.set_blob(path.path, oid)
+                except pysvn.ClientError:
+                    # probably a directory; create an empty file named '.'
+                    data = ''
+                    oid = sha1(data).hexdigest()
+                    root.set_blob(path.path + '/.', oid)
+        return root
+
+    def _refresh_tree(self, tree, obj):
+        tree.object_ids=Object(
+            (o.hex(), name)
+            for name, o in obj.trees.iteritems())
+        tree.object_ids.update(
+            (oid, name)
+            for name, oid in obj.blobs.iteritems())
+        for name, o in obj.trees.iteritems():
+            subtree, isnew = M.Tree.upsert('svn', o.hex())
+            if isnew:
+                subtree.set_context(tree, name)
+                subtree.set_last_commit(tree.commit)
+                self._refresh_tree(subtree, o)
+        for name, oid in obj.blobs.iteritems():
+            blob, isnew = M.Blob.upsert('svn', oid)
+            if isnew:
+                blob.set_context(tree, name)
+                blob.set_last_commit(tree.commit)
+
+    def _revno(self, oid):
+        return int(oid.split(':')[1])
+
+    def _oid(self, revno):
+        return '%s:%s' % (self._repo._id, revno)
+
 MappedClass.compile_all()