* This is mostly done now, although not really documented.
* AJAX editing of source files (avoid page refreshes)
* Add a better page for attempting to list a missing directory
* Extend the identification of files without extensions to try a pygments
lexer. If one isn't found, or if we find ReST, use the rest formatter, if
it finds other wiki formats we know about, use those, otherwise format using
the lexer it finds, and if none found, then plain (or treat as wiki
page, yet to decide).
* Allow specific extensions for certain formatters (.rst, .md, .mdwn, etc)
* Support a readonly mode
Standalone requirements
-----------------------
* Sessions
* Login and logout
* SQLite storage engine for usernames and passwords?
* Subscriptions?
* Generate a nonce for new users, and validate email addresses
(hmm... requires incoming email processing, not that easy for standalone
deployments, and not needed for launchpad integration)
Running behind a reverse proxy
------------------------------
Support getting credentials from elsewhere, and updating author accordingly.
Launchpad integration
---------------------
* A plan for spam! Really needed as we have spammers
* How to handle merges
Page merge hooks?
Ideas not yet fully formed
--------------------------
* Lazy loading of formatters (to reduce install dependancies)
* Customize default rendering engine
* Extend creole to allow specifying format of embedded code
* Look for a wiki-media python library for formatting wiki-media text
Or write one.
* Soundex filename search based on base_name of the files, used when
hitting a missing file to offer suggestions.
wikkid-0.5/bin/ 0000775 0000000 0000000 00000000000 14345210116 0013405 5 ustar 00root root 0000000 0000000 wikkid-0.5/bin/_wikkid_path.py 0000664 0000000 0000000 00000000620 14345210116 0016412 0 ustar 00root root 0000000 0000000 #! /usr/bin/python
#
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""Add the parent directory to the python path to run the scripts."""
import sys
import os.path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
wikkid-0.5/bin/wikkid-serve 0000775 0000000 0000000 00000007046 14345210116 0015746 0 ustar 00root root 0000000 0000000 #! /usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""The server class for the wiki."""
try:
import _wikkid_path
except ImportError:
# Not running from a branch.
pass
import logging
import optparse
import sys
from wikkid import version
from wikkid.app import WikkidApp
from wikkid.context import (
DEFAULT_FORMAT,
DEFAULT_HOST,
DEFAULT_PORT,
ExecutionContext,
)
def setup_logging():
"""Set up a logger sending to stderr."""
handler = logging.StreamHandler(sys.stderr)
fmt = '%(asctime)s %(levelname)-7s %(message)s'
formatter = logging.Formatter(
fmt=fmt, datefmt="%Y-%m-%d %H:%M:%S")
handler.setFormatter(formatter)
root = logging.getLogger()
root.addHandler(handler)
def main(args):
usage = "Usage: %prog [options] "
parser = optparse.OptionParser(
usage=usage, description="Run a Wikkid Wiki server.", version=version)
parser.add_option('--format', type='choice', default='bzr',
choices=['bzr', 'git'], help=("Default repository format to use."))
parser.add_option(
'--host', type='string', default=DEFAULT_HOST,
help=('The interface to listen on. Defaults to %r' % DEFAULT_HOST))
parser.add_option(
'--port', type='int', default=DEFAULT_PORT,
help=('The port to listen on. Defaults to %s.' % DEFAULT_PORT))
parser.add_option(
'--default-format', type='string', default=DEFAULT_FORMAT,
help=("Specify the default wiki format to use. Defaults to %r"
% DEFAULT_FORMAT))
parser.add_option(
'--script-name',
help=('The SCRIPT_NAME for the environment. This is the prefix for the URLs'))
options, args = parser.parse_args(sys.argv[1:])
execution_context = ExecutionContext(
host=options.host,
port=options.port,
default_format=options.default_format,
script_name=options.script_name)
if len(args) == 0:
print("No branch location specified.")
parser.print_usage()
sys.exit(1)
if len(args) > 1:
print("Unexpected positional args: %s" % args[1:])
parser.print_usage()
sys.exit(1)
branch = args[0]
setup_logging()
logger = logging.getLogger('wikkid')
logger.setLevel(logging.INFO)
if options.format == 'bzr':
from breezy.workingtree import WorkingTree
from wikkid.filestore.bzr import FileStore
from wikkid.user.bzr import LocalBazaarUserMiddleware
import breezy.bzr
import breezy.git
working_tree = WorkingTree.open(branch)
logger.info('Using: %s', working_tree)
filestore = FileStore(working_tree)
elif options.format == 'git':
from wikkid.filestore.git import FileStore
from wikkid.user.git import LocalGitUserMiddleware
filestore = FileStore.from_path(branch)
app = WikkidApp(filestore=filestore, execution_context=execution_context)
if options.format == 'bzr':
app = LocalBazaarUserMiddleware(app, working_tree.branch)
elif options.format == 'git':
app = LocalGitUserMiddleware(app, filestore.repo)
from wsgiref.simple_server import make_server
httpd = make_server(options.host, options.port, app)
logger.info('Serving on http://%s:%s', options.host, options.port)
try:
httpd.serve_forever()
except KeyboardInterrupt:
logger.info('Done.')
if __name__ == "__main__":
main(sys.argv)
wikkid-0.5/disperse.conf 0000664 0000000 0000000 00000000304 14345210116 0015317 0 ustar 00root root 0000000 0000000 # See https://github.com/jelmer/disperse
timeout_days: 5
tag_name: "v$VERSION"
update_version {
path: "wikkid/__init__.py"
match: "^version = \"(.*)\"$"
new_line: "version = \"$VERSION\""
}
wikkid-0.5/plugin/ 0000775 0000000 0000000 00000000000 14345210116 0014133 5 ustar 00root root 0000000 0000000 wikkid-0.5/plugin/__init__.py 0000664 0000000 0000000 00000000227 14345210116 0016245 0 ustar 00root root 0000000 0000000 from breezy.commands import plugin_cmds
plugin_cmds.register_lazy('cmd_wikkid', ['wiki'],
'breezy.plugins.wikkid.commands')
wikkid-0.5/plugin/commands.py 0000664 0000000 0000000 00000003113 14345210116 0016304 0 ustar 00root root 0000000 0000000 import logging
import sys
from breezy.commands import Command
from breezy.option import Option
from breezy.workingtree import WorkingTree
from wikkid.app import WikkidApp
from wikkid.filestore.bzr import FileStore
from wikkid.user.bzr import LocalBazaarUserMiddleware
DEFAULT_PORT = 8080
def setup_logging():
"""Set up a logger sending to stderr."""
handler = logging.StreamHandler(sys.stderr)
fmt = '%(asctime)s %(levelname)-7s %(message)s'
formatter = logging.Formatter(
fmt=fmt, datefmt="%Y-%m-%d %H:%M:%S")
handler.setFormatter(formatter)
root = logging.getLogger()
root.addHandler(handler)
class cmd_wikkid(Command):
"""Serve branch as a wiki using wikkid."""
aliases = ['wiki']
takes_args = ['branch?']
takes_options = [
Option(
'port',
help='Port to listen, defaults to 8080.',
type=int,
short_name='p')
]
def run(self, branch=u'.', port=8080):
setup_logging()
logger = logging.getLogger('wikkid')
logger.setLevel(logging.DEBUG)
working_tree = WorkingTree.open(branch)
logger.info('Using: %s', working_tree)
filestore = FileStore(working_tree)
app = WikkidApp(filestore)
app = LocalBazaarUserMiddleware(app, working_tree.branch)
from wsgiref.simple_server import make_server
httpd = make_server('localhost', port, app)
logger.info('Serving on http://localhost:%s', port)
try:
httpd.serve_forever()
except KeyboardInterrupt:
logger.info('Done.')
wikkid-0.5/pyproject.toml 0000664 0000000 0000000 00000000121 14345210116 0015543 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
wikkid-0.5/setup.cfg 0000664 0000000 0000000 00000001621 14345210116 0014456 0 ustar 00root root 0000000 0000000 [metadata]
name = Wikkid
description = VCS-backed wiki
long_description = A wiki that is backed by Gitr or Bazaar that allows local branching of the wiki
for later merging. Also doesn't have any page locks and uses three way merging.
version = attr:wikkid.version
author = Wikkid Developers
author_email = wikkid-dev@lists.launchpad.net
url = https://launchpad.net/wikkid
project_urls =
Repository=https://github.com/wikkid-team/wikkid
[options]
scripts = bin/wikkid-serve
packages = find:
package_dir = breezy.plugins.wikkid=plugin
include_package_data = True
install_requires =
breezy
docutils
dulwich
jinja2
merge3
pygments
twisted
webob
zope.interface
tests_require =
testtools
bs4
lxml
[options.extras_require]
dev =
testtools
bs4
lxml
[options.package_data]
wikkid/skin =
default/*.html
default/favicon.ico
default/static/*
wikkid-0.5/setup.py 0000775 0000000 0000000 00000000153 14345210116 0014351 0 ustar 00root root 0000000 0000000 #!/usr/bin/python3
from setuptools import setup
setup(data_files=[('share/man/man1', ['wikkid-serve.1'])])
wikkid-0.5/tox.ini 0000664 0000000 0000000 00000000216 14345210116 0014147 0 ustar 00root root 0000000 0000000 [tox]
downloadcache = {toxworkdir}/cache/
[testenv]
commands = python3 -m testtools.run wikkid.tests.test_suite
extras = dev
recreate = True
wikkid-0.5/wikkid-serve.1 0000664 0000000 0000000 00000001461 14345210116 0015325 0 ustar 00root root 0000000 0000000 .TH WIKKID-SERVE "1" "January 2011" "wikkid-serve 0.2dev" "User Commands"
.SH NAME
wikkid-serve \- Serve a wiki from a Bazaar branch
.SH SYNOPSIS
.B wikkid-serve
[\fIoptions\fR] \fI\fR
.SH DESCRIPTION
Run a stand-alone Wikkid Wiki server.
.SH OPTIONS
.TP
\fB\-\-version\fR
show program's version number and exit
.TP
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-\-host\fR=\fIHOST\fR
The interface to listen on. Defaults to 'localhost'
.TP
\fB\-\-port\fR=\fIPORT\fR
The port to listen on. Defaults to 8080.
.TP
\fB\-\-default\-format\fR=\fIDEFAULT_FORMAT\fR
Specify the default wiki format to use. Defaults to
\&'rest'
.SH "LICENSE"
Wikkid is licensed under the
.B GNU Affero General Public License
.SH "SEE ALSO"
The wikkid home page can be found at
.B https://launchpad.net/wikkid
wikkid-0.5/wikkid/ 0000775 0000000 0000000 00000000000 14345210116 0014117 5 ustar 00root root 0000000 0000000 wikkid-0.5/wikkid/__init__.py 0000664 0000000 0000000 00000000305 14345210116 0016226 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A Distributed Wiki."""
version = "0.5"
wikkid-0.5/wikkid/app.py 0000664 0000000 0000000 00000011004 14345210116 0015245 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A WSGI application for Wikkid."""
import logging
import mimetypes
import os.path
import urllib.parse
from wsgiref.util import shift_path_info
from breezy import urlutils
from webob import Request, Response
from webob.exc import HTTPException, HTTPNotFound
from wikkid.context import ExecutionContext
from wikkid.dispatcher import get_view
from wikkid.fileutils import FileIterable
from wikkid.model.factory import ResourceFactory
from wikkid.skin.loader import Skin
from wikkid.view.urls import parse_url
def serve_file(filename):
if os.path.exists(filename):
basename = urlutils.basename(filename)
content_type = mimetypes.guess_type(basename)[0]
res = Response(content_type=content_type, conditional_response=True)
res.app_iter = FileIterable(filename)
res.content_length = os.path.getsize(filename)
res.last_modified = os.path.getmtime(filename)
# Todo: is this the best value for the etag?
# perhaps md5 would be a better alternative
res.etag = '%s-%s-%s' % (
os.path.getmtime(filename), os.path.getsize(filename),
hash(filename))
return res
else:
return HTTPNotFound()
class WikkidApp(object):
"""The main wikkid application."""
def __init__(self, filestore, skin_name=None, execution_context=None):
if execution_context is None:
execution_context = ExecutionContext()
self.execution_context = execution_context
self.filestore = filestore
self.resource_factory = ResourceFactory(self.filestore)
# Need to load the initial templates for the skin.
if skin_name is None:
skin_name = 'default'
self.skin = Skin(skin_name)
self.logger = logging.getLogger('wikkid')
def preprocess_environ(self, environ):
request = Request(environ)
path = urllib.parse.unquote(request.path)
script_name = self.execution_context.script_name
# Firstly check to see if the path is the same as the script_name
if path != script_name and not path.startswith(script_name + '/'):
raise HTTPNotFound()
shifted_prefix = ''
while shifted_prefix != script_name:
shifted = shift_path_info(environ)
shifted_prefix = '{0}/{1}'.format(shifted_prefix, shifted)
# Now we are just interested in the path_info having ignored the
# script name.
path = urllib.parse.unquote(request.path_info)
if path == '':
path = '/' # Explicitly be the root (we need the /)
return request, path
def _get_view(self, request, path):
"""Get the view for the path specified."""
resource_path, action = parse_url(path)
model = self.resource_factory.get_resource_at_path(resource_path)
return get_view(model, action, request, self.execution_context)
def process_call(self, environ):
"""The actual implementation of dealing with the call."""
# TODO: reject requests that aren't GET or POST
try:
request, path = self.preprocess_environ(environ)
except HTTPException as e:
return e
if path == '/favicon.ico':
if self.skin.favicon is not None:
return serve_file(self.skin.favicon)
else:
return HTTPNotFound()
if path.startswith('/static/'):
if self.skin.static_dir is not None:
static_dir = self.skin.static_dir.rstrip(os.sep) + os.sep
static_file = os.path.abspath(
urlutils.joinpath(static_dir, path[8:]))
if static_file.startswith(static_dir):
return serve_file(static_file)
else:
return HTTPNotFound()
else:
return HTTPNotFound()
try:
view = self._get_view(request, path)
return view.render(self.skin)
except HTTPException as e:
return e
def get_view(self, environ):
"""Allow an app user to get the wikkid view for a particular call."""
request, path = self.preprocess_environ(environ)
return self._get_view(request, path)
def __call__(self, environ, start_response):
"""The WSGI bit."""
response = self.process_call(environ)
return response(environ, start_response)
wikkid-0.5/wikkid/context.py 0000664 0000000 0000000 00000002477 14345210116 0016167 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright (C) 2010-2012 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A means of storing execution context."""
DEFAULT_FORMAT = 'rest'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8080
class ExecutionContext(object):
"""Store run-time execution context data.
This is the Encapsulate Context pattern.
"""
def __init__(self, host=None, port=None, default_format=None,
script_name=None):
"""Create an execution context for the application.
:param host: The hostname that content is being served from.
:param port: The port that is being listened on.
:param default_format: The default wiki format for pages that
don't specify any.
"""
if host is None:
host = DEFAULT_HOST
if port is None:
port = DEFAULT_PORT
if default_format is None:
default_format = DEFAULT_FORMAT
self.host = host
self.port = port
self.default_format = default_format
# TODO: make sure the script_name if set starts with a slash and
# doesn't finish with one.
if script_name is None:
script_name = ''
self.script_name = script_name.rstrip('/')
wikkid-0.5/wikkid/contrib/ 0000775 0000000 0000000 00000000000 14345210116 0015557 5 ustar 00root root 0000000 0000000 wikkid-0.5/wikkid/contrib/__init__.py 0000664 0000000 0000000 00000000316 14345210116 0017670 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""For packages that are not adequately packaged."""
wikkid-0.5/wikkid/dispatcher.py 0000664 0000000 0000000 00000006257 14345210116 0016631 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""The dispatcher for wikkid views.
When this module is loaded, it will automagically load all the other views in
this directory. The views inherit from the BaseView which has a metaclass
which registers the view with the dispatcher.
"""
import os
from breezy.urlutils import dirname, joinpath
from zope.interface import providedBy
from wikkid.context import ExecutionContext
# The view registry needs to map an Interface and a name to a class.
_VIEW_REGISTRY = {}
def get_view(obj, view_name, request, ec=ExecutionContext()):
"""Get the most relevant view for the object for the specified name.
Iterate through the provided interfaces of the object and look in the view
registry for a view.
"""
interfaces = providedBy(obj)
for interface in interfaces:
try:
klass = _VIEW_REGISTRY[(interface, view_name)]
instance = klass(obj, request, ec)
instance.initialize()
return instance
except KeyError:
pass
# For example, if someone asked for 'raw' view on a directory or binary
# object.
return None
def register_view(view_class):
"""Register the view."""
interface = getattr(view_class, 'for_interface', None)
view_name = getattr(view_class, 'name', None)
default_view = getattr(view_class, 'is_default', False)
if view_name is None or interface is None:
# Don't register.
return
key = (interface, view_name)
assert key not in _VIEW_REGISTRY, "key already registered: %r" % (key,)
_VIEW_REGISTRY[key] = view_class
if default_view:
_VIEW_REGISTRY[(interface, None)] = view_class
def unregister_view(view_class):
"""Unregister the view."""
interface = getattr(view_class, 'for_interface', None)
view_name = getattr(view_class, 'name', None)
default_view = getattr(view_class, 'is_default', False)
if view_name is None or interface is None:
# Don't register.
return
key = (interface, view_name)
assert _VIEW_REGISTRY[key] is view_class, \
"key registered with different class: %r: %r != %r" % (
key, _VIEW_REGISTRY[key], view_class)
del _VIEW_REGISTRY[key]
if default_view:
del _VIEW_REGISTRY[(interface, None)]
# We know that the controller (whatever that is going to end up being) will
# load this module to get the 'get_view' function. None of the other view
# modules should be explicitly loaded anywhere else (possible exceptions may
# occur, so this isn't a hard rule).
#
# So... when this module is loaded, we want to load the other modules in the
# wikkid.view package so that when the classes are parsed, they register
# themselves with the view registry.
def load_view_modules():
curr_dir = os.path.abspath(dirname(__file__))
view_dir = joinpath(curr_dir, 'view')
py_files = [
filename for filename in os.listdir(view_dir)
if filename.endswith('.py') and not filename.startswith('__')]
for filename in py_files:
__import__('wikkid.view.%s' % filename[:-3])
load_view_modules()
wikkid-0.5/wikkid/filestore/ 0000775 0000000 0000000 00000000000 14345210116 0016113 5 ustar 00root root 0000000 0000000 wikkid-0.5/wikkid/filestore/__init__.py 0000664 0000000 0000000 00000001134 14345210116 0020223 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""Wikkid filestores.
The volatile filestore is primarily used for tests, and the bzr filestore is
where all the main work happens.
"""
class FileExists(Exception):
"""A file was found where a directory is wanted."""
class UpdateConflicts(Exception):
"""Conflicts were found during updating."""
def __init__(self, content, basis_rev):
Exception.__init__(self)
self.content = content
self.basis_rev = basis_rev
wikkid-0.5/wikkid/filestore/basefile.py 0000664 0000000 0000000 00000001426 14345210116 0020242 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""Base classes for other filestores to use."""
import mimetypes
import breezy.urlutils as urlutils
from wikkid.interface.filestore import FileType
class BaseFile(object):
"""Provide common fields and methods and properties for files."""
def __init__(self, path):
self.path = path
self.base_name = urlutils.basename(path)
self._mimetype = mimetypes.guess_type(self.base_name)[0]
@property
def mimetype(self):
"""If the file_type is a directory, return None."""
if self.file_type == FileType.DIRECTORY:
return None
else:
return self._mimetype
wikkid-0.5/wikkid/filestore/bzr.py 0000664 0000000 0000000 00000031230 14345210116 0017261 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A bzr backed filestore."""
from datetime import datetime
import logging
from zope.interface import implementer
from merge3 import Merge3
from breezy.bzr.generate_ids import gen_file_id
from breezy.errors import BinaryFile
from breezy.osutils import splitpath, split_lines
from breezy.revision import NULL_REVISION
from breezy.textfile import check_text_lines
from breezy.transform import FinalPaths, MalformedTransform
from breezy.urlutils import basename, dirname, joinpath
from wikkid.filestore import FileExists, UpdateConflicts
from wikkid.filestore.basefile import BaseFile
from wikkid.interface.filestore import FileType, IFile, IFileStore
def normalize_line_endings(content, ending=b'\n'):
return ending.join(content.splitlines())
def get_line_ending(lines):
"""Work out the line ending used in lines."""
if len(lines) == 0:
return b'\n'
first = lines[0]
if first.endswith(b'\r\n'):
return b'\r\n'
# Default to \n if there are no line endings.
return b'\n'
def get_commit_message(commit_message):
if commit_message is None or commit_message.strip() == '':
return 'No description of change given.'
return commit_message
def normalize_content(content):
# Default to simple '\n' line endings.
content = normalize_line_endings(content)
# Make sure the content ends with a new-line. This makes
# end of file conflicts nicer.
if not content.endswith(b'\n'):
content += b'\n'
return content
def iter_paths(path):
path_segments = splitpath(path)
while len(path_segments) > 0:
tail = path_segments.pop()
if len(path_segments) == 0:
yield '', tail
else:
yield joinpath(*path_segments), tail
def create_parents(tt, path, trans_id):
prev_trans_id = trans_id
for parent_path, tail in iter_paths(path):
trans_id = tt.trans_id_tree_path(parent_path)
if tt.tree_kind(trans_id) is not None:
break
tt.adjust_path(tail, trans_id, prev_trans_id)
tt.create_directory(trans_id)
tt.version_file(trans_id=trans_id, file_id=gen_file_id(basename(path)))
prev_trans_id = trans_id
@implementer(IFileStore)
class FileStore(object):
"""Wraps a Bazaar branch to be a filestore."""
def __init__(self, tree):
self.tree = tree
self.branch = tree.branch
self.logger = logging.getLogger('wikkid')
def basis_tree(self):
return self.tree.basis_tree()
def get_file(self, path):
"""Return an object representing the file at specified path."""
if not self.tree.is_versioned(path):
return None
else:
return File(self, path)
def update_file(self, path, content, author, parent_revision,
commit_message=None):
"""Update the file at the specified path with the content.
This is going to be really interesting when we need to deal with
conflicts.
"""
commit_message = get_commit_message(commit_message)
if parent_revision is None:
parent_revision = NULL_REVISION
# Firstly we want to lock the tree for writing.
with self.tree.lock_write():
# Look to see if the path is there. If it is then we are doing an
# update. If it isn't we are doing an add.
if self.tree.is_versioned(path):
# What if a parent_revision hasn't been set?
self._update_file(
path, content, author, parent_revision,
commit_message)
else:
self._add_file(path, content, author, commit_message)
def _ensure_directory_or_nonexistant(self, dir_path):
"""Ensure the dir_path defines a directory or doesn't exist.
Walk up the dir_path and make sure that the path either doesn't exist
at all, or is a directory. The purpose of this is to make sure we
don't try to add a file in a directory where the directory has the
same name as an existing file.
"""
check = []
while dir_path:
check.append(dir_path)
dir_path = dirname(dir_path)
while len(check):
f = self.get_file(check.pop())
if f is not None:
if not f.is_directory:
raise FileExists(
'%s exists and is not a directory' % f.path)
def _add_file(self, path, content, author, commit_message):
"""Add a new file at the specified path with the content.
Then commit this new file with the specified commit_message.
"""
content = normalize_content(content)
t = self.tree.controldir.root_transport
# Get a transport for the path we want.
self._ensure_directory_or_nonexistant(dirname(path))
t = t.clone(dirname(path))
t.create_prefix()
# Put the file there.
# TODO: UTF-8 encode text files?
t.put_bytes(basename(path), content)
self.tree.smart_add([t.local_abspath('.')])
self.tree.commit(
message=commit_message,
authors=[author])
def _get_final_text(self, content, f, parent_revision):
current_rev = f.last_modified_in_revision
wt = self.tree
current_lines = wt.get_file_lines(f.path)
basis = self.branch.repository.revision_tree(parent_revision)
basis_lines = basis.get_file_lines(f.path)
# need to break content into lines.
ending = get_line_ending(current_lines)
# If the content doesn't end with a new line, add one.
new_lines = split_lines(content)
# Look at the end of the first string.
new_ending = get_line_ending(new_lines)
if ending != new_ending:
# I know this is horribly inefficient, but lets get it working
# first.
content = normalize_line_endings(content, ending)
new_lines = split_lines(content)
if len(new_lines) > 0 and not new_lines[-1].endswith(ending):
new_lines[-1] += ending
merge = Merge3(basis_lines, new_lines, current_lines)
result = b''.join(merge.merge_lines()) # or merge_regions or whatever
conflicted = (b'>>>>>>>' + ending) in result
if conflicted:
raise UpdateConflicts(result, current_rev)
return result
def _update_file(self, path, content, author, parent_revision,
commit_message):
"""Update an existing file with the content.
This method merges the changes in based on the parent revision.
"""
f = File(self, path)
wt = self.tree
with wt.lock_write():
result = self._get_final_text(content, f, parent_revision)
wt.controldir.root_transport.put_bytes(path, result)
wt.commit(
message=commit_message, authors=[author],
specific_files=[path])
def list_directory(self, directory_path):
"""Return a list of File objects for in the directory path.
If the path doesn't exist, returns None. If the path exists but is
empty, an empty list is returned. Otherwise a list of File objects in
that directory.
"""
if directory_path is not None:
directory = self.get_file(directory_path)
if directory is None or directory.file_type != FileType.DIRECTORY:
return None
listing = []
wt = self.tree
with wt.lock_read():
for fp, fc, fkind, entry in wt.list_files(
from_dir=directory_path, recursive=False):
if fc != 'V':
# If the file isn't versioned, skip it.
continue
if directory_path is None:
file_path = fp
else:
file_path = joinpath(directory_path, fp)
listing.append(File(self, file_path))
return listing
@implementer(IFile)
class File(BaseFile):
"""Represents a file in the Bazaar branch."""
def __init__(self, filestore, path):
BaseFile.__init__(self, path)
self.filestore = filestore
# This isn't entirely necessary.
self.tree = self.filestore.tree
self.file_type = self._get_filetype()
self._last_modified_in_revision = None
def _get_filetype(self):
"""Work out the filetype based on the mimetype if possible."""
with self.tree.lock_read():
is_directory = ('directory' == self.tree.kind(self.path))
if is_directory:
return FileType.DIRECTORY
else:
if self._mimetype is None:
binary = self._is_binary
else:
binary = not self._mimetype.startswith('text/')
if binary:
return FileType.BINARY_FILE
else:
return FileType.TEXT_FILE
def get_content(self):
with self.tree.lock_read():
# basis_tree is a revision tree, queries the repositry.
# to get the stuff off the filesystem use the working tree
# which needs to start with that. WorkingTree.open('.').
# branch = tree.branch.
return self.tree.get_file_text(self.path)
@property
def last_modified_in_revision(self):
if self._last_modified_in_revision is None:
try:
self._last_modified_in_revision = self.tree.get_file_revision(
self.path)
except AttributeError:
bt = self.tree.basis_tree()
self._last_modified_in_revision = bt.get_file_revision(
self.path)
return self._last_modified_in_revision
@property
def last_modified_by(self):
"""Return the first author for the revision."""
repo = self.filestore.branch.repository
rev = repo.get_revision(self.last_modified_in_revision)
return rev.get_apparent_authors()[0]
@property
def last_modified_date(self):
"""Return the last modified date for the revision."""
repo = self.filestore.branch.repository
rev = repo.get_revision(self.last_modified_in_revision)
return datetime.utcfromtimestamp(rev.timestamp)
@property
def _is_binary(self):
"""True if the file is binary."""
try:
with self.tree.lock_read():
lines = self.tree.get_file_lines(self.path)
check_text_lines(lines)
return False
except BinaryFile:
return True
@property
def is_directory(self):
"""Is this file a directory?"""
return 'directory' == self.tree.kind(self.path)
def update(self, content, user):
raise NotImplementedError()
class BranchFileStore(FileStore):
def __init__(self, branch):
self.branch = branch
self.tree = branch.basis_tree()
self.logger = logging.getLogger('wikkid')
def basis_tree(self):
return self.tree
def update_file(self, path, content, author, parent_revision,
commit_message=None):
commit_message = get_commit_message(commit_message)
with self.branch.lock_write():
if self.tree.is_versioned(path):
f = File(self, path)
content = self._get_final_text(content, f, parent_revision)
else:
content = normalize_content(content)
if not isinstance(content, bytes):
raise TypeError(content)
with self.tree.preview_transform() as tt:
trans_id = tt.trans_id_tree_path(path)
if tt.tree_kind(trans_id) is not None:
tt.delete_contents(trans_id)
else:
name = splitpath(path)[-1]
tt.version_file(
trans_id=trans_id, file_id=gen_file_id(name))
create_parents(tt, path, trans_id)
tt.create_file([content], trans_id)
try:
tt.commit(self.branch, commit_message, authors=[author])
except MalformedTransform as e:
for conflict in e.conflicts:
if conflict[0] == 'non-directory parent':
path = FinalPaths(tt).get_path(trans_id)
raise FileExists(
'%s exists and is not a directory' %
conflict[1])
raise
self.tree = self.branch.basis_tree()
wikkid-0.5/wikkid/filestore/git.py 0000664 0000000 0000000 00000015671 14345210116 0017262 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2012 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A git filestore using Dulwich.
"""
import datetime
import mimetypes
from dulwich.objects import Blob, Tree, ZERO_SHA
from dulwich.object_store import tree_lookup_path
from dulwich.repo import Repo
from dulwich.walk import Walker
import posixpath
import stat
from zope.interface import implementer
from wikkid.filestore import FileExists, UpdateConflicts
from wikkid.interface.filestore import FileType, IFile, IFileStore
@implementer(IFileStore)
class FileStore(object):
"""A filestore that just uses an internal map to store data."""
_encoding = 'utf-8'
@classmethod
def from_path(cls, path):
return cls(Repo(path))
def __init__(self, repo, ref=b'HEAD'):
"""Repo is a dulwich repository."""
self.repo = repo
self.ref = ref
@property
def store(self):
return self.repo.object_store
def _get_root(self, revision=None):
if revision is None:
try:
revision = self.repo.refs[self.ref]
except KeyError:
revision = ZERO_SHA
try:
return (revision, self.repo[revision].tree)
except KeyError:
return None, None
def get_file(self, path):
"""Return an object representing the file."""
commit_id, root_id = self._get_root()
if root_id is None:
return None
try:
(mode, sha) = tree_lookup_path(
self.store.__getitem__,
root_id, path.encode(self._encoding))
except KeyError:
return None
return File(
self.store, mode, sha, path, commit_id, encoding=self._encoding)
def update_file(self, path, content, author, parent_revision,
commit_message=None):
"""The `author` is updating the file at `path` with `content`."""
commit_id, root_id = self._get_root()
if root_id is None:
root_tree = Tree()
else:
root_tree = self.store[root_id]
# Find all tree objects involved
tree = root_tree
trees = [root_tree]
elements = path.strip(posixpath.sep).split(posixpath.sep)
for el in elements[:-1]:
try:
(mode, sha) = tree[el.encode(self._encoding)]
except KeyError:
tree = Tree()
else:
if not stat.S_ISDIR(mode):
raise FileExists(
"File %s exists and is not a directory" % el)
tree = self.store[sha]
trees.append(tree)
if elements[-1] in tree:
(old_mode, old_sha) = tree[elements[-1]]
if stat.S_ISDIR(old_mode):
raise FileExists("File %s exists and is a directory" % path)
if old_sha != parent_revision and parent_revision is not None:
raise UpdateConflicts(
"File conflict %s != %s" % (
old_sha, parent_revision), old_sha)
if not isinstance(content, bytes):
raise TypeError(content)
blob = Blob.from_string(content)
child = (stat.S_IFREG | 0o644, blob.id)
self.store.add_object(blob)
assert len(trees) == len(elements)
for tree, name in zip(reversed(trees), reversed(elements)):
assert name != ""
tree[name.encode(self._encoding)] = child
self.store.add_object(tree)
child = (stat.S_IFDIR, tree.id)
if commit_message is None:
commit_message = ""
if author is not None:
author = author.encode(self._encoding)
self.repo.do_commit(
ref=self.ref, message=commit_message.encode(self._encoding),
author=author, tree=child[1])
def list_directory(self, directory_path):
"""Return a list of File objects for in the directory path.
If the path doesn't exist, returns None. If the path exists but is
empty, an empty list is returned. Otherwise a list of File objects in
that directory.
"""
if directory_path is None:
directory_path = ''
else:
directory_path = directory_path.strip(posixpath.sep)
commit_id, root_id = self._get_root()
if directory_path == '':
sha = root_id
mode = stat.S_IFDIR
else:
if root_id is None:
return None
try:
(mode, sha) = tree_lookup_path(
self.store.__getitem__,
root_id, directory_path.encode(self._encoding))
except KeyError:
return None
if mode is not None and stat.S_ISDIR(mode):
ret = []
for (name, mode, sha) in self.store[sha].items():
ret.append(
File(self.store, mode, sha,
posixpath.join(
directory_path, name.decode(self._encoding)),
commit_id, encoding=self._encoding))
return ret
else:
return None
@implementer(IFile)
class File(object):
"""A Git file object."""
def __init__(self, store, mode, sha, path, commit_sha, encoding):
self.store = store
self.encoding = encoding
self.mode = mode
self.sha = sha
self.path = path
self.commit_sha = commit_sha
self.base_name = posixpath.basename(path)
self.mimetype = mimetypes.guess_type(self.base_name)[0]
@property
def file_type(self):
"""Work out the filetype based on the mimetype if possible."""
if self._is_directory:
return FileType.DIRECTORY
else:
if self.mimetype is None:
binary = self._is_binary
else:
binary = not self.mimetype.startswith('text/')
if binary:
return FileType.BINARY_FILE
else:
return FileType.TEXT_FILE
def get_content(self):
o = self.store[self.sha]
if isinstance(o, Blob):
return o.data
else:
return None
@property
def _is_directory(self):
return stat.S_ISDIR(self.mode)
@property
def _is_binary(self):
return b'\0' in self.get_content()
def _get_last_modified_commit(self):
walker = Walker(
self.store, include=[self.commit_sha],
paths=[self.path.encode(self.encoding)])
return next(iter(walker)).commit
@property
def last_modified_in_revision(self):
return self.sha
@property
def last_modified_by(self):
return self._get_last_modified_commit().author.decode(self.encoding)
@property
def last_modified_date(self):
c = self._get_last_modified_commit()
return datetime.datetime.utcfromtimestamp(c.commit_time)
wikkid-0.5/wikkid/filestore/volatile.py 0000664 0000000 0000000 00000011424 14345210116 0020306 0 ustar 00root root 0000000 0000000 #
# Copyright (C) 2010 Wikkid Developers.
#
# This software is licensed under the GNU Affero General Public License
# version 3 (see the file LICENSE).
"""A volatile filestore.
Used primarily for test purposes, this class should be a fully functional
filestore, albiet one that doesn't remember anything persistently.
"""
from datetime import datetime
from itertools import count
from breezy.urlutils import dirname
from zope.interface import implementer
from wikkid.filestore import FileExists
from wikkid.filestore.basefile import BaseFile
from wikkid.interface.filestore import FileType, IFile, IFileStore
@implementer(IFileStore)
class FileStore(object):
"""A filestore that just uses an internal map to store data."""
def __init__(self, files=None):
"""Files is a list of tuples.
If the content is None, the path is assumed to be a directory. If the
content contains a null character, the file is considered binary.
"""
self._integer = count(1)
self.file_id_map = {}
self.path_map = {}
if files is None:
files = []
user = 'First User