changeset 72:4d157afdb560

Added support for Mercurial command server as backend.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 27 Feb 2013 21:50:26 -0800
parents 1d004c53f2b2
children d0be251663b5
files wikked/scm.py wikked/views.py wikked/wiki.py
diffstat 3 files changed, 129 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/wikked/scm.py	Wed Feb 27 07:06:35 2013 -0800
+++ b/wikked/scm.py	Wed Feb 27 21:50:26 2013 -0800
@@ -1,6 +1,7 @@
 import re
 import os
 import os.path
+import time
 import logging
 import tempfile
 import subprocess
@@ -51,6 +52,7 @@
 class Revision(object):
     def __init__(self, rev_id=-1):
         self.rev_id = rev_id
+        self.rev_name = rev_id
         self.author = None
         self.timestamp = 0
         self.description = None
@@ -65,19 +67,11 @@
         return self.rev_id != -1
 
 
-class MercurialSourceControl(SourceControl):
+class MercurialBaseSourceControl(SourceControl):
     def __init__(self, root, logger=None):
         SourceControl.__init__(self, logger)
         self.root = root
 
-        self.hg = 'hg'
-        self.log_style = os.path.join(os.path.dirname(__file__), 'resources', 'hg_log.style')
-        self.actions = {
-                'A': ACTION_ADD,
-                'R': ACTION_DELETE,
-                'M': ACTION_EDIT
-                }
-
     def initRepo(self):
         # Make a Mercurial repo if there's none.
         if not os.path.isdir(os.path.join(self.root, '.hg')):
@@ -97,11 +91,33 @@
         specials = ['.hg', '.hgignore', '.hgtags']
         return [os.path.join(self.root, d) for d in specials]
 
+    def _run(self, cmd, *args, **kwargs):
+        exe = [self.hg]
+        if 'norepo' not in kwargs or not kwargs['norepo']:
+            exe += ['-R', self.root]
+        exe.append(cmd)
+        exe += args
+        self.logger.debug("Running Mercurial: " + str(exe))
+        return subprocess.check_output(exe)
+
+
+class MercurialSourceControl(SourceControl):
+    def __init__(self, root, logger=None):
+        MercurialBaseSourceControl.__init__(self, root, logger)
+
+        self.hg = 'hg'
+        self.log_style = os.path.join(os.path.dirname(__file__), 'resources', 'hg_log.style')
+        self.actions = {
+                'A': ACTION_ADD,
+                'R': ACTION_DELETE,
+                'M': ACTION_EDIT
+                }
+
     def getHistory(self, path=None):
         if path is not None:
             st_out = self._run('status', path)
             if len(st_out) > 0 and st_out[0] == '?':
-                return [Revision()]
+                return []
 
         log_args = []
         if path is not None:
@@ -178,6 +194,7 @@
 
         rev = Revision()
         rev.rev_id = int(m.group(1))
+        rev.rev_name = rev.rev_id[:12]
         rev.rev_hash = m.group(2)
         rev.author = m.group(3)
         rev.timestamp = float(m.group(4))
@@ -198,11 +215,83 @@
 
         return rev
 
-    def _run(self, cmd, *args, **kwargs):
-        exe = [self.hg]
-        if 'norepo' not in kwargs or not kwargs['norepo']:
-            exe += ['-R', self.root]
-        exe.append(cmd)
-        exe += args
-        self.logger.debug("Running Mercurial: " + str(exe))
-        return subprocess.check_output(exe)
+
+class MercurialCommandServerSourceControl(MercurialBaseSourceControl):
+    def __init__(self, root, logger=None):
+        MercurialBaseSourceControl.__init__(self, root, logger)
+
+        import hglib
+        self.client = hglib.open(self.root)
+
+    def getHistory(self, path=None):
+        if path is not None:
+            rel_path = os.path.relpath(path, self.root)
+            status = self.client.status(include=rel_path)
+            if len(status) > 0 and status[0] == '?':
+                return []
+
+        if path is not None:
+            repo_revs = self.client.log(files=[path])
+        else:
+            repo_revs = self.client.log()
+        revisions = []
+        for rev in repo_revs:
+            r = Revision(rev.node)
+            r.rev_name = rev.node[:12]
+            r.author = rev.author
+            r.timestamp = time.mktime(rev.date.timetuple())
+            r.description = rev.desc
+            revisions.append(r)
+        return revisions
+
+    def getState(self, path):
+        rel_path = os.path.relpath(path, self.root)
+        statuses = self.client.status(include=rel_path)
+        if len(statuses) == 0:
+            return STATE_COMMITTED
+        status = statuses[0]
+        if status[0] == '?' or status[0] == 'A':
+            return STATE_NEW
+        if status[0] == 'M':
+            return STATE_MODIFIED
+        raise Exception("Unsupported status: %s" % status)
+            
+    def getRevision(self, path, rev):
+        rel_path = os.path.relpath(path, self.root)
+        return self.client.cat(rel_path, rev=rev)
+
+    def diff(self, path, rev1, rev2):
+        rel_path = os.path.relpath(path, self.root)
+        if rev2 is None:
+            return self.client.diff(files=[rel_path], change=rev1, git=True)
+        return self.client.diff(files=[rel_path], revs=[rev1, rev2], git=True)
+
+    def commit(self, paths, op_meta):
+        if 'message' not in op_meta or not op_meta['message']:
+            raise ValueError("No commit message specified.")
+
+        # Get repo-relative paths.
+        rel_paths = [os.path.relpath(p, self.root) for p in paths]
+
+        # Check if any of those paths needs to be added.
+        status = self.client.status(unknown=True)
+        add_paths = []
+        for s in status:
+            if s[1] in rel_paths:
+                add_paths.append(s[1])
+        if len(add_paths) > 0:
+            self.client.add(*add_paths)
+
+        # Commit!
+        if 'author' in op_meta:
+            self.client.commit(*rel_paths, message=op_meta['message'], user=op_meta['author'])
+        else:
+            self.client.commit(*rel_paths, message=op_meta['message'])
+
+    def revert(self, paths=None):
+        if paths is not None:
+            rel_paths = [os.path.relpath(p, self.root) for p in paths]
+            self.client.revert(*rel_paths, clean=True)
+        else:
+            self.client.revert(all=True, clean=True)
+
--- a/wikked/views.py	Wed Feb 27 07:06:35 2013 -0800
+++ b/wikked/views.py	Wed Feb 27 21:50:26 2013 -0800
@@ -61,30 +61,33 @@
     return meta
 
 
-def get_history_data(history):
+def get_history_data(history, needs_files=False):
     hist_data = []
     for i, rev in enumerate(reversed(history)):
         rev_data = {
             'index': i + 1,
             'rev_id': rev.rev_id,
-            'rev_hash': rev.rev_hash,
+            'rev_name': rev.rev_name,
             'author': rev.author,
             'timestamp': rev.timestamp,
-            'description': rev.description,
-            'pages': []
+            'description': rev.description
             }
-        for f in rev.files:
-            f_info = g.wiki.fs.getPageInfo(f['path'])
-            if f_info is None:
-                continue
-            page = g.wiki.getPage(f_info['url'])
-            if not is_page_readable(page):
-                continue
-            rev_data['pages'].append({
-                'url': f_info['url'],
-                'action': scm.ACTION_NAMES[f['action']]
-                })
-        if len(rev_data['pages']) > 0:
+        if needs_files:
+            rev_data['pages'] = []
+            for f in rev.files:
+                f_info = g.wiki.fs.getPageInfo(f['path'])
+                if f_info is None:
+                    continue
+                page = g.wiki.getPage(f_info['url'])
+                if not is_page_readable(page):
+                    continue
+                rev_data['pages'].append({
+                    'url': f_info['url'],
+                    'action': scm.ACTION_NAMES[f['action']]
+                    })
+            if len(rev_data['pages']) > 0:
+                hist_data.append(rev_data)
+        else:
             hist_data.append(rev_data)
     return hist_data
 
@@ -311,7 +314,7 @@
 @app.route('/api/history')
 def api_site_history():
     history = g.wiki.getHistory()
-    hist_data = get_history_data(history)
+    hist_data = get_history_data(history, needs_files=True)
     result = {'history': hist_data}
     return make_auth_response(result)
 
--- a/wikked/wiki.py	Wed Feb 27 07:06:35 2013 -0800
+++ b/wikked/wiki.py	Wed Feb 27 21:50:26 2013 -0800
@@ -8,7 +8,7 @@
 from page import Page, DatabasePage
 from fs import FileSystem
 from db import SQLiteDatabase, conn_scope
-from scm import MercurialSourceControl
+from scm import MercurialCommandServerSourceControl
 from indexer import WhooshWikiIndex
 from auth import UserManager
 
@@ -62,7 +62,7 @@
     def scm_factory(self, config):
         scm_type = config.get('wiki', 'scm')
         if scm_type == 'hg':
-            return MercurialSourceControl(self.root, logger=self.logger_factory())
+            return MercurialCommandServerSourceControl(self.root, logger=self.logger_factory())
         else:
             raise InitializationError("No such source control: " + scm_type)