import os import re import ghdiff import gittle.utils import yaml from gittle import Gittle from dulwich.repo import NotGitRepository from realms.lib.util import to_canonical, cname_to_filename, filename_to_cname from realms import cache from realms.lib.hook import HookMixin class PageNotFound(Exception): pass class Wiki(HookMixin): path = None base_path = '/' default_ref = 'master' default_committer_name = 'Anon' default_committer_email = 'anon@anon.anon' index_page = 'home' gittle = None repo = None def __init__(self, path): try: self.gittle = Gittle(path) except NotGitRepository: self.gittle = Gittle.init(path) # Dulwich repo self.repo = self.gittle.repo self.path = path def __repr__(self): return "Wiki: %s" % self.path def _get_user(self, username, email): if not username: username = self.default_committer_name if not email: email = self.default_committer_email return username, email def revert_page(self, name, commit_sha, message, username, email): """Revert page to passed commit sha1 :param name: Name of page to revert. :param commit_sha: Commit Sha1 to revert to. :param message: Commit message. :param username: Committer name. :param email: Committer email. :return: Git commit sha1 """ page = self.get_page(name, commit_sha) if not page: raise PageNotFound('Commit not found') if not message: commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')]) message = commit_info['message'] return self.write_page(name, page['data'], message=message, username=username, email=email) def write_page(self, name, content, message=None, create=False, username=None, email=None): """Write page to git repo :param name: Name of page. :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. """ cname = to_canonical(name) filename = cname_to_filename(cname) with open(self.path + "/" + filename, 'w') as f: f.write(content) if create: self.gittle.add(filename) if not message: message = "Updated %s" % name username, email = self._get_user(username, email) ret = self.gittle.commit(name=username, email=email, message=message, files=[filename]) cache.delete(cname) return ret def rename_page(self, old_name, new_name, username=None, email=None, message=None): """Rename page. :param old_name: Page that will be renamed. :param new_name: New name of page. :param username: Committer name :param email: Committer email :return: str -- Commit sha1 """ old_filename, new_filename = map(cname_to_filename, [old_name, new_name]) if old_filename not in self.gittle.index: # old doesn't exist return None if new_filename in self.gittle.index: # file is being overwritten, but that is ok, it's git! pass username, email = self._get_user(username, email) if not message: message = "Moved %s to %s" % (old_name, new_name) os.rename(os.path.join(self.path, old_filename), os.path.join(self.path, new_filename)) self.gittle.add(new_filename) self.gittle.rm(old_filename) commit = self.gittle.commit(name=username, email=email, message=message, files=[old_filename, new_filename]) cache.delete_many(old_name, new_name) return commit def delete_page(self, name, username=None, email=None, message=None): """Delete page. :param name: Page that will be deleted :param username: Committer name :param email: Committer email :return: str -- Commit sha1 """ username, email = self._get_user(username, email) if not message: message = "Deleted %s" % name filename = cname_to_filename(name) self.gittle.rm(filename) commit = self.gittle.commit(name=username, email=email, message=message, files=[str(filename)]) cache.delete_many(name) return commit def get_page(self, name, sha='HEAD'): """Get page data, partials, commit info. :param name: Name of page. :param sha: Commit sha. :return: dict """ cached = cache.get(name) if cached: return cached # commit = gittle.utils.git.commit_info(self.repo[sha]) filename = cname_to_filename(name).encode('latin-1') sha = sha.encode('latin-1') try: data = self.gittle.get_commit_files(sha, paths=[filename]).get(filename) if not data: return None partials = {} if data.get('data'): meta = self.get_meta(data['data']) if meta and 'import' in meta: for partial_name in meta['import']: partials[partial_name] = self.get_page(partial_name) data['partials'] = partials data['info'] = self.get_history(name, limit=1)[0] return data except KeyError: # HEAD doesn't exist yet return None def get_meta(self, content): """Get metadata from page if any. :param content: Page content :return: dict """ if not content.startswith("---"): return None meta_end = re.search("\n(\.{3}|\-{3})", content) if not meta_end: return None try: return yaml.safe_load(content[0:meta_end.start()]) except Exception as e: return {'error': e.message} def compare(self, name, old_sha, new_sha): """Compare two revisions of the same page. :param name: Name of page. :param old_sha: Older sha. :param new_sha: Newer sha. :return: str - Raw markup with styles """ # TODO: This could be effectively done in the browser old = self.get_page(name, sha=old_sha) new = self.get_page(name, sha=new_sha) return ghdiff.diff(old['data'], new['data']) def get_index(self): """Get repo index of head. :return: list -- List of dicts """ 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)) return rv def get_history(self, name, limit=100): """Get page history. :param name: Name of page. :param limit: Limit history size. :return: list -- List of dicts """ if not len(self.repo.open_index()): # Index is empty, no commits return [] file_path = cname_to_filename(name) versions = [] walker = self.repo.get_walker(paths=[file_path], max_entries=limit) for entry in walker: change_type = None for change in entry.changes(): if change.old.path == file_path: change_type = change.type elif change.new.path == file_path: change_type = change.type author_name, author_email = entry.commit.author.split('<') versions.append(dict( author=author_name.strip(), time=entry.commit.author_time, message=entry.commit.message, sha=entry.commit.id, type=change_type)) return versions