2016-07-07 09:49:52 +03:00
|
|
|
import collections
|
|
|
|
import itertools
|
2013-10-02 07:32:53 +03:00
|
|
|
import os
|
2015-12-09 04:38:09 +02:00
|
|
|
import posixpath
|
2013-10-08 06:06:54 +03:00
|
|
|
import re
|
2013-10-03 17:58:07 +03:00
|
|
|
import ghdiff
|
2013-10-15 23:32:17 +03:00
|
|
|
import gittle.utils
|
2014-09-07 19:54:51 +03:00
|
|
|
import yaml
|
2013-09-29 00:09:02 +03:00
|
|
|
from gittle import Gittle
|
2016-07-06 03:29:01 +03:00
|
|
|
from dulwich.object_store import tree_lookup_path
|
2013-10-05 00:42:45 +03:00
|
|
|
from dulwich.repo import NotGitRepository
|
2016-07-06 03:29:01 +03:00
|
|
|
from realms.lib.util import cname_to_filename, filename_to_cname
|
2014-08-30 18:06:12 +03:00
|
|
|
from realms import cache
|
2014-10-09 06:42:29 +03:00
|
|
|
from realms.lib.hook import HookMixin
|
2013-09-29 00:09:02 +03:00
|
|
|
|
2013-09-29 00:33:00 +03:00
|
|
|
|
2014-10-21 01:27:38 +03:00
|
|
|
class PageNotFound(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2014-10-09 06:42:29 +03:00
|
|
|
class Wiki(HookMixin):
|
2013-09-29 00:09:02 +03:00
|
|
|
path = None
|
|
|
|
base_path = '/'
|
|
|
|
default_ref = 'master'
|
|
|
|
default_committer_name = 'Anon'
|
|
|
|
default_committer_email = 'anon@anon.anon'
|
2013-10-02 04:50:48 +03:00
|
|
|
index_page = 'home'
|
2014-09-12 01:35:17 +03:00
|
|
|
gittle = None
|
2013-09-29 00:33:00 +03:00
|
|
|
repo = None
|
2013-09-29 00:09:02 +03:00
|
|
|
|
2013-10-01 07:10:10 +03:00
|
|
|
def __init__(self, path):
|
|
|
|
try:
|
2014-09-12 01:35:17 +03:00
|
|
|
self.gittle = Gittle(path)
|
2013-10-05 00:42:45 +03:00
|
|
|
except NotGitRepository:
|
2014-09-12 01:35:17 +03:00
|
|
|
self.gittle = Gittle.init(path)
|
|
|
|
|
|
|
|
# Dulwich repo
|
|
|
|
self.repo = self.gittle.repo
|
2013-10-01 07:10:10 +03:00
|
|
|
|
2013-09-29 00:09:02 +03:00
|
|
|
self.path = path
|
|
|
|
|
2014-09-03 17:26:53 +03:00
|
|
|
def __repr__(self):
|
|
|
|
return "Wiki: %s" % self.path
|
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def get_page(self, name, sha='HEAD'):
|
|
|
|
"""Get page data, partials, commit info.
|
2014-10-22 00:06:27 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:param name: Name of page.
|
|
|
|
:param sha: Commit sha.
|
|
|
|
:return: dict
|
2014-10-22 00:06:27 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
"""
|
|
|
|
return WikiPage(name, self, sha=sha)
|
2015-12-11 08:56:27 +02:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def get_index(self):
|
|
|
|
"""Get repo index of head.
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:return: list -- List of dicts
|
2014-10-21 01:27:38 +03:00
|
|
|
|
|
|
|
"""
|
2016-07-05 05:22:12 +03:00
|
|
|
rv = []
|
|
|
|
index = self.repo.open_index()
|
|
|
|
for name in index:
|
|
|
|
rv.append(dict(name=filename_to_cname(name),
|
|
|
|
filename=name,
|
|
|
|
ctime=index[name].ctime[0],
|
|
|
|
mtime=index[name].mtime[0],
|
|
|
|
sha=index[name].sha,
|
|
|
|
size=index[name].size))
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
return rv
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2013-10-15 23:32:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
class WikiPage(object):
|
|
|
|
def __init__(self, name, wiki, sha='HEAD'):
|
|
|
|
self.name = name
|
|
|
|
self.filename = cname_to_filename(name)
|
|
|
|
self.sha = sha.encode('latin-1')
|
|
|
|
self.wiki = wiki
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
@property
|
|
|
|
def data(self):
|
|
|
|
cache_key = self._cache_key('data')
|
|
|
|
cached = cache.get(cache_key)
|
|
|
|
if cached:
|
|
|
|
return cached
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
data = self.wiki.gittle.get_commit_files(self.sha, paths=[self.filename]).get(self.filename).get('data')
|
|
|
|
cache.set(cache_key, data)
|
|
|
|
return data
|
2015-12-09 04:38:09 +02:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
@property
|
|
|
|
def info(self):
|
|
|
|
cache_key = self._cache_key('info')
|
|
|
|
cached = cache.get(cache_key)
|
|
|
|
if cached:
|
|
|
|
return cached
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
info = self.get_history(limit=1)[0]
|
|
|
|
cache.set(cache_key, info)
|
|
|
|
return info
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-07 09:49:52 +03:00
|
|
|
@property
|
|
|
|
def history(self):
|
2016-07-05 05:22:12 +03:00
|
|
|
"""Get page history.
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:return: list -- List of dicts
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
"""
|
2016-07-07 09:49:52 +03:00
|
|
|
return PageHistory(self, self._cache_key('history'))
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
@property
|
|
|
|
def partials(self):
|
|
|
|
data = self.data
|
|
|
|
if not data:
|
|
|
|
return {}
|
|
|
|
partials = {}
|
|
|
|
meta = self._get_meta(data)
|
|
|
|
if meta and 'import' in meta:
|
|
|
|
for partial_name in meta['import']:
|
|
|
|
partials[partial_name] = self.wiki.get_page(partial_name, sha=self.sha)
|
|
|
|
return partials
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_meta(content):
|
|
|
|
"""Get metadata from page if any.
|
2014-08-30 18:06:12 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:param content: Page content
|
|
|
|
:return: dict
|
2013-09-29 00:09:02 +03:00
|
|
|
|
2014-10-21 01:27:38 +03:00
|
|
|
"""
|
2016-07-05 05:22:12 +03:00
|
|
|
if not content.startswith("---"):
|
2014-09-12 01:35:17 +03:00
|
|
|
return None
|
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
meta_end = re.search("\n(\.{3}|\-{3})", content)
|
2014-09-12 01:35:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not meta_end:
|
|
|
|
return None
|
2014-10-22 00:06:27 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
try:
|
|
|
|
return yaml.safe_load(content[0:meta_end.start()])
|
|
|
|
except Exception as e:
|
|
|
|
return {'error': e.message}
|
2014-10-22 00:06:27 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def _cache_key(self, property):
|
|
|
|
return 'page/{0}[{1}].{2}'.format(self.name, self.sha, property)
|
2014-09-12 01:35:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def _get_user(self, username, email):
|
|
|
|
if not username:
|
|
|
|
username = self.wiki.default_committer_name
|
2014-09-12 01:35:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not email:
|
|
|
|
email = self.wiki.default_committer_email
|
2014-09-12 01:35:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
return username, email
|
2013-09-29 00:09:02 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def _clear_cache(self):
|
|
|
|
cache.delete_many(self._cache_key(p) for p in ['data', 'info'])
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def delete(self, username=None, email=None, message=None):
|
2014-10-21 01:27:38 +03:00
|
|
|
"""Delete page.
|
2014-10-22 00:06:27 +03:00
|
|
|
:param username: Committer name
|
|
|
|
:param email: Committer email
|
2014-10-21 01:27:38 +03:00
|
|
|
:return: str -- Commit sha1
|
|
|
|
|
|
|
|
"""
|
2014-10-22 00:06:27 +03:00
|
|
|
username, email = self._get_user(username, email)
|
|
|
|
|
|
|
|
if not message:
|
2016-07-05 05:22:12 +03:00
|
|
|
message = "Deleted %s" % self.name
|
2015-09-28 07:57:56 +03:00
|
|
|
|
|
|
|
# gittle.rm won't actually remove the file, have to do it ourselves
|
2016-07-05 05:22:12 +03:00
|
|
|
os.remove(os.path.join(self.wiki.path, self.filename))
|
|
|
|
self.wiki.gittle.rm(self.filename)
|
|
|
|
commit = self.wiki.gittle.commit(name=username,
|
|
|
|
email=email,
|
|
|
|
message=message,
|
|
|
|
files=[self.filename])
|
|
|
|
self._clear_cache()
|
2014-10-21 01:27:38 +03:00
|
|
|
return commit
|
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def rename(self, new_name, username=None, email=None, message=None):
|
|
|
|
"""Rename page.
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:param new_name: New name of page.
|
|
|
|
:param username: Committer name
|
|
|
|
:param email: Committer email
|
|
|
|
:return: str -- Commit sha1
|
2014-10-21 01:27:38 +03:00
|
|
|
|
|
|
|
"""
|
2016-07-05 05:22:12 +03:00
|
|
|
assert self.sha == 'HEAD'
|
|
|
|
old_filename, new_filename = self.filename, cname_to_filename(new_name)
|
|
|
|
if old_filename not in self.wiki.gittle.index:
|
|
|
|
# old doesn't exist
|
|
|
|
return None
|
|
|
|
elif old_filename == new_filename:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
# file is being overwritten, but that is ok, it's git!
|
|
|
|
pass
|
2014-09-03 17:26:53 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
username, email = self._get_user(username, email)
|
2014-08-20 18:28:25 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not message:
|
|
|
|
message = "Moved %s to %s" % (self.name, new_name)
|
2013-10-02 04:50:48 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
os.rename(os.path.join(self.wiki.path, old_filename), os.path.join(self.wiki.path, new_filename))
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
self.wiki.gittle.add(new_filename)
|
|
|
|
self.wiki.gittle.rm(old_filename)
|
|
|
|
|
|
|
|
commit = self.wiki.gittle.commit(name=username,
|
|
|
|
email=email,
|
|
|
|
message=message,
|
|
|
|
files=[old_filename, new_filename])
|
|
|
|
|
|
|
|
self._clear_cache()
|
|
|
|
self.name = new_name
|
|
|
|
self.filename = new_filename
|
|
|
|
# We need to clear the cache for the new name as well as the old
|
|
|
|
self._clear_cache()
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
return commit
|
|
|
|
|
|
|
|
def write(self, content, message=None, create=False, username=None, email=None):
|
|
|
|
"""Write page to git repo
|
|
|
|
|
|
|
|
:param content: Content of page.
|
|
|
|
:param message: Commit message.
|
|
|
|
:param create: Perform git add operation?
|
|
|
|
:param username: Commit Name.
|
|
|
|
:param email: Commit Email.
|
|
|
|
:return: Git commit sha1.
|
2014-10-21 01:27:38 +03:00
|
|
|
"""
|
2016-07-05 05:22:12 +03:00
|
|
|
assert self.sha == 'HEAD'
|
|
|
|
dirname = posixpath.join(self.wiki.path, posixpath.dirname(self.filename))
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not os.path.exists(dirname):
|
|
|
|
os.makedirs(dirname)
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
with open(self.wiki.path + "/" + self.filename, 'w') as f:
|
|
|
|
f.write(content)
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if create:
|
|
|
|
self.wiki.gittle.add(self.filename)
|
2014-09-07 19:54:51 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not message:
|
|
|
|
message = "Updated %s" % self.name
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
username, email = self._get_user(username, email)
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
ret = self.wiki.gittle.commit(name=username,
|
|
|
|
email=email,
|
|
|
|
message=message,
|
|
|
|
files=[self.filename])
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
self._clear_cache()
|
|
|
|
return ret
|
2013-10-03 17:58:07 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def revert(self, commit_sha, message, username, email):
|
|
|
|
"""Revert page to passed commit sha1
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:param commit_sha: Commit Sha1 to revert to.
|
|
|
|
:param message: Commit message.
|
|
|
|
:param username: Committer name.
|
|
|
|
:param email: Committer email.
|
|
|
|
:return: Git commit sha1
|
2014-10-21 01:27:38 +03:00
|
|
|
|
|
|
|
"""
|
2016-07-05 05:22:12 +03:00
|
|
|
assert self.sha == 'HEAD'
|
|
|
|
new_page = self.wiki.get_page(self.name, commit_sha)
|
|
|
|
if not new_page:
|
|
|
|
raise PageNotFound('Commit not found')
|
2014-10-09 23:47:12 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
if not message:
|
|
|
|
commit_info = gittle.utils.git.commit_info(self.wiki.gittle[commit_sha.encode('latin-1')])
|
|
|
|
message = commit_info['message']
|
2014-10-09 23:47:12 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
return self.write(new_page.data, message=message, username=username, email=email)
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def compare(self, old_sha):
|
|
|
|
"""Compare two revisions of the same page.
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
:param old_sha: Older sha.
|
|
|
|
:return: str - Raw markup with styles
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
"""
|
2014-10-21 01:27:38 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
# TODO: This could be effectively done in the browser
|
|
|
|
old = self.wiki.get_page(self.name, sha=old_sha)
|
|
|
|
return ghdiff.diff(old.data, self.data)
|
2014-09-12 01:35:17 +03:00
|
|
|
|
2016-07-05 05:22:12 +03:00
|
|
|
def __nonzero__(self):
|
2016-07-06 03:29:01 +03:00
|
|
|
# Verify this file is in the tree for the given commit sha
|
|
|
|
try:
|
|
|
|
tree_lookup_path(self.wiki.repo.get_object, self.wiki.repo[self.sha].tree, self.filename)
|
|
|
|
except KeyError:
|
|
|
|
# We'll get a KeyError if self.sha isn't in the repo, or if self.filename isn't in the tree of our commit
|
|
|
|
return False
|
|
|
|
return True
|
2016-07-07 09:49:52 +03:00
|
|
|
|
|
|
|
|
|
|
|
class PageHistory(collections.Sequence):
|
|
|
|
"""Acts like a list, but dynamically loads and caches history revisions as requested."""
|
|
|
|
def __init__(self, page, cache_key):
|
|
|
|
self.page = page
|
|
|
|
self.cache_key = cache_key
|
|
|
|
self._store = cache.get(cache_key) or []
|
|
|
|
if not self._store:
|
|
|
|
self._iter_rest = self._get_rest()
|
|
|
|
elif self._store[-1] == 'TAIL':
|
|
|
|
self._iter_rest = None
|
|
|
|
else:
|
|
|
|
self._iter_rest = self._get_rest(self._store[-1]['sha'])
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
# Iterate over the revisions already cached
|
|
|
|
for r in self._store:
|
|
|
|
if r == 'TAIL':
|
|
|
|
return
|
|
|
|
yield r
|
|
|
|
# Iterate over the revisions yet to be discovered
|
|
|
|
if self._iter_rest:
|
|
|
|
try:
|
|
|
|
for r in self._iter_rest:
|
|
|
|
self._store.append(r)
|
|
|
|
yield r
|
|
|
|
self._store.append('TAIL')
|
|
|
|
finally:
|
|
|
|
# Make sure we cache the results whether or not the iteration was completed
|
|
|
|
cache.set(self.cache_key, self._store)
|
|
|
|
|
|
|
|
def _get_rest(self, start_sha=None):
|
|
|
|
if not len(self.page.wiki.repo.open_index()):
|
|
|
|
# Index is empty, no commits
|
|
|
|
return
|
|
|
|
walker = iter(self.page.wiki.repo.get_walker(paths=[self.page.filename], include=start_sha, follow=True))
|
|
|
|
if start_sha:
|
|
|
|
# If we are not starting from HEAD, we already have the start commit
|
|
|
|
print(next(walker))
|
|
|
|
filename = self.page.filename
|
|
|
|
for entry in walker:
|
|
|
|
change_type = None
|
|
|
|
for change in entry.changes():
|
|
|
|
if change.new.path == filename:
|
|
|
|
filename = change.old.path
|
|
|
|
change_type = change.type
|
|
|
|
break
|
|
|
|
|
|
|
|
author_name, author_email = entry.commit.author.rstrip('>').split('<')
|
|
|
|
r = dict(author=author_name.strip(),
|
|
|
|
author_email=author_email,
|
|
|
|
time=entry.commit.author_time,
|
|
|
|
message=entry.commit.message,
|
|
|
|
sha=entry.commit.id,
|
|
|
|
type=change_type)
|
|
|
|
yield r
|
|
|
|
|
|
|
|
def __getitem__(self, index):
|
|
|
|
if isinstance(index, slice):
|
|
|
|
return list(itertools.islice(self, index.start, index.stop, index.step))
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
return next(itertools.islice(self, index, index+1))
|
|
|
|
except StopIteration:
|
|
|
|
raise IndexError
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
if not self._store or self._store[-1] != 'TAIL':
|
|
|
|
# Force generation of all revisions
|
|
|
|
list(self)
|
|
|
|
return len(self._store) - 1 # Don't include the TAIL sentinel
|