Merge lp:~jelmer/wikkid/dulwich into lp:wikkid

Proposed by Jelmer Vernooij
Status: Merged
Merged at revision: 74
Proposed branch: lp:~jelmer/wikkid/dulwich
Merge into: lp:wikkid
Diff against target: 522 lines (+362/-16)
12 files modified
bin/wikkid-serve (+18/-8)
setup.py (+1/-0)
wikkid/dispatcher.py (+1/-1)
wikkid/filestore/basefile.py (+1/-2)
wikkid/filestore/bzr.py (+2/-1)
wikkid/filestore/git.py (+205/-0)
wikkid/filestore/volatile.py (+2/-1)
wikkid/interface/filestore.py (+0/-3)
wikkid/tests/__init__.py (+1/-0)
wikkid/tests/filestore.py (+2/-0)
wikkid/tests/test_git_filestore.py (+55/-0)
wikkid/user/git.py (+74/-0)
To merge this branch: bzr merge lp:~jelmer/wikkid/dulwich
Reviewer Review Type Date Requested Status
Tim Penhey Approve
Gavin Panella Approve
Review via email: mp+183373@code.launchpad.net

Description of the change

Add a dulwich-based filestore backend to wikkid, which can be used for git repositories.

To post a comment you must log in.
lp:~jelmer/wikkid/dulwich updated
76. By Jelmer Vernooij

Simplify commit, add --format=git option.

77. By Jelmer Vernooij

Fix user id.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

ping, anyone?

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

ping (-:

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

ping :-)

Revision history for this message
Gavin Panella (allenap) wrote :

I'm not sure that I'm really qualified to review this, but cosmic
radiation seems to have flipped a bit in Launchpad's database such that
I have been invited to pass judgement.

[1]

+            try:
+                (mode, sha) = tree[el]
+                if not stat.S_ISDIR(mode):
+                    raise FileExists(
+                        "File %s exists and is not a directory" % el)
+            except KeyError:
+                tree = Tree()
+            else:
+                tree = self.store[sha]

I'm guessing that you're not trying to guard against a KeyError in the
`if not stat.S_ISDIR(...` block, so it probably ought to go in the else:
block.

[2]

+                ret.append(
+                    File(self.store, mode, sha, posixpath.join(directory_path, name), commit_id))

In places you use "/" as the separator. Consider using posixpath.sep
for, I don't know, cleanliness?

[3]

Can you add dulwich as a dependency in setup.py?

[4]

There are a lot of new things here without obvious tests, like the
middleware, but given that there's little other development activity I
hardly want to block for that. In other words, you're very naughty for
not testing things, but there's no punishment, and thanks for the
contribution :)

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Thanks for the review, Gavin. Much appreciated! :-)

I'll follow up with some fixes when I get back from vacation.

lp:~jelmer/wikkid/dulwich updated
78. By Jelmer Vernooij

Merge trunk.

79. By Jelmer Vernooij

Add dulwich dependency.

80. By Jelmer Vernooij

Use posixpath.sep rather than /

81. By Jelmer Vernooij

Move extra code out of try/except.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Thanks, I've now fixed the issues you pointed out.

Revision history for this message
Tim Penhey (thumper) wrote :

On 16/01/14 13:06, Jelmer Vernooij wrote:
> Thanks, I've now fixed the issues you pointed out.

Now I have to remember what this is all about and how to run the tests :-)

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On Thu, Jan 16, 2014 at 12:18:16AM -0000, Tim Penhey wrote:
> On 16/01/14 13:06, Jelmer Vernooij wrote:
> > Thanks, I've now fixed the issues you pointed out.
>
> Now I have to remember what this is all about and how to run the tests :-)

FWIW I get two failing tests, but those happen under trunk as well. :-)

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

ping? :)

Revision history for this message
Tim Penhey (thumper) wrote :

Guys, I've been pretty crap at following up, but both Jelmer and Gavin have commit rights to trunk :-)

Trunk is owned by ~wikkid. Jelmer, you are good to merge.

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Thanks, Tim! Merged.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bin/wikkid-serve'
--- bin/wikkid-serve 2012-05-17 09:32:49 +0000
+++ bin/wikkid-serve 2014-01-16 00:05:37 +0000
@@ -18,8 +18,6 @@
18import optparse18import optparse
19import sys19import sys
2020
21from bzrlib.workingtree import WorkingTree
22
23from wikkid import version21from wikkid import version
24from wikkid.app import WikkidApp22from wikkid.app import WikkidApp
25from wikkid.context import (23from wikkid.context import (
@@ -28,8 +26,6 @@
28 DEFAULT_PORT,26 DEFAULT_PORT,
29 ExecutionContext,27 ExecutionContext,
30 )28 )
31from wikkid.filestore.bzr import FileStore
32from wikkid.user.bzr import LocalBazaarUserMiddleware
3329
3430
35def setup_logging():31def setup_logging():
@@ -48,6 +44,8 @@
48 usage = "Usage: %prog [options] <wiki-branch>"44 usage = "Usage: %prog [options] <wiki-branch>"
49 parser = optparse.OptionParser(45 parser = optparse.OptionParser(
50 usage=usage, description="Run a Wikkid Wiki server.", version=version)46 usage=usage, description="Run a Wikkid Wiki server.", version=version)
47 parser.add_option('--format', type='choice', default='bzr',
48 choices=['bzr', 'git'], help=("Default repository format to use."))
51 parser.add_option(49 parser.add_option(
52 '--host', type='string', default=DEFAULT_HOST,50 '--host', type='string', default=DEFAULT_HOST,
53 help=('The interface to listen on. Defaults to %r' % DEFAULT_HOST))51 help=('The interface to listen on. Defaults to %r' % DEFAULT_HOST))
@@ -82,12 +80,24 @@
82 logger = logging.getLogger('wikkid')80 logger = logging.getLogger('wikkid')
83 logger.setLevel(logging.INFO)81 logger.setLevel(logging.INFO)
8482
85 working_tree = WorkingTree.open(branch)83 if options.format == 'bzr':
86 logger.info('Using: %s', working_tree)84 from bzrlib.workingtree import WorkingTree
87 filestore = FileStore(working_tree)85 from wikkid.filestore.bzr import FileStore
86 from wikkid.user.bzr import LocalBazaarUserMiddleware
87
88 working_tree = WorkingTree.open(branch)
89 logger.info('Using: %s', working_tree)
90 filestore = FileStore(working_tree)
91 elif options.format == 'git':
92 from wikkid.filestore.git import FileStore
93 from wikkid.user.git import LocalGitUserMiddleware
94 filestore = FileStore.from_path(branch)
8895
89 app = WikkidApp(filestore=filestore, execution_context=execution_context)96 app = WikkidApp(filestore=filestore, execution_context=execution_context)
90 app = LocalBazaarUserMiddleware(app, working_tree.branch)97 if options.format == 'bzr':
98 app = LocalBazaarUserMiddleware(app, working_tree.branch)
99 elif options.format == 'git':
100 app = LocalGitUserMiddleware(app, filestore.repo)
91 from wsgiref.simple_server import make_server101 from wsgiref.simple_server import make_server
92 httpd = make_server(options.host, options.port, app)102 httpd = make_server(options.host, options.port, app)
93 logger.info('Serving on http://%s:%s', options.host, options.port)103 logger.info('Serving on http://%s:%s', options.host, options.port)
94104
=== modified file 'setup.py'
--- setup.py 2012-05-17 10:28:31 +0000
+++ setup.py 2014-01-16 00:05:37 +0000
@@ -24,6 +24,7 @@
24 include_package_data=True,24 include_package_data=True,
25 install_requires=[25 install_requires=[
26 'docutils',26 'docutils',
27 'dulwich',
27 'jinja2',28 'jinja2',
28 'pygments',29 'pygments',
29 'twisted',30 'twisted',
3031
=== modified file 'wikkid/dispatcher.py'
--- wikkid/dispatcher.py 2010-11-12 09:00:42 +0000
+++ wikkid/dispatcher.py 2014-01-16 00:05:37 +0000
@@ -53,7 +53,7 @@
53 # Don't register.53 # Don't register.
54 return54 return
55 key = (interface, view_name)55 key = (interface, view_name)
56 assert key not in _VIEW_REGISTRY, "key already registered: %r" % key56 assert key not in _VIEW_REGISTRY, "key already registered: %r" % (key,)
57 _VIEW_REGISTRY[key] = view_class57 _VIEW_REGISTRY[key] = view_class
58 if default_view:58 if default_view:
59 _VIEW_REGISTRY[(interface, None)] = view_class59 _VIEW_REGISTRY[(interface, None)] = view_class
6060
=== modified file 'wikkid/filestore/basefile.py'
--- wikkid/filestore/basefile.py 2010-05-12 10:39:13 +0000
+++ wikkid/filestore/basefile.py 2014-01-16 00:05:37 +0000
@@ -16,9 +16,8 @@
16class BaseFile(object):16class BaseFile(object):
17 """Provide common fields and methods and properties for files."""17 """Provide common fields and methods and properties for files."""
1818
19 def __init__(self, path, file_id):19 def __init__(self, path):
20 self.path = path20 self.path = path
21 self.file_id = file_id
22 self.base_name = urlutils.basename(path)21 self.base_name = urlutils.basename(path)
23 self._mimetype = mimetypes.guess_type(self.base_name)[0]22 self._mimetype = mimetypes.guess_type(self.base_name)[0]
2423
2524
=== modified file 'wikkid/filestore/bzr.py'
--- wikkid/filestore/bzr.py 2012-06-14 01:14:00 +0000
+++ wikkid/filestore/bzr.py 2014-01-16 00:05:37 +0000
@@ -243,7 +243,8 @@
243 implements(IFile)243 implements(IFile)
244244
245 def __init__(self, filestore, path, file_id):245 def __init__(self, filestore, path, file_id):
246 BaseFile.__init__(self, path, file_id)246 BaseFile.__init__(self, path)
247 self.file_id = file_id
247 self.filestore = filestore248 self.filestore = filestore
248 # This isn't entirely necessary.249 # This isn't entirely necessary.
249 self.tree = self.filestore.tree250 self.tree = self.filestore.tree
250251
=== added file 'wikkid/filestore/git.py'
--- wikkid/filestore/git.py 1970-01-01 00:00:00 +0000
+++ wikkid/filestore/git.py 2014-01-16 00:05:37 +0000
@@ -0,0 +1,205 @@
1#
2# Copyright (C) 2012 Wikkid Developers.
3#
4# This software is licensed under the GNU Affero General Public License
5# version 3 (see the file LICENSE).
6
7"""A git filestore using Dulwich.
8
9"""
10
11import datetime
12import mimetypes
13
14from dulwich.objects import Blob, Tree, ZERO_SHA
15from dulwich.object_store import tree_lookup_path
16from dulwich.repo import Repo
17from dulwich.walk import Walker
18import posixpath
19import stat
20
21from zope.interface import implements
22
23from wikkid.errors import FileExists, UpdateConflicts
24from wikkid.interface.filestore import FileType, IFile, IFileStore
25
26
27class FileStore(object):
28 """A filestore that just uses an internal map to store data."""
29
30 implements(IFileStore)
31
32 @classmethod
33 def from_path(cls, path):
34 return cls(Repo(path))
35
36 def __init__(self, repo, ref='HEAD'):
37 """Repo is a dulwich repository."""
38 self.repo = repo
39 self.ref = ref
40
41 @property
42 def store(self):
43 return self.repo.object_store
44
45 def _get_root(self, revision=None):
46 if revision is None:
47 try:
48 revision = self.repo.refs[self.ref]
49 except KeyError:
50 revision = ZERO_SHA
51 try:
52 return (revision, self.repo[revision].tree)
53 except KeyError:
54 return None, None
55
56 def get_file(self, path):
57 """Return an object representing the file."""
58 commit_id, root_id = self._get_root()
59 if root_id is None:
60 return None
61 try:
62 (mode, sha) = tree_lookup_path(self.store.__getitem__,
63 root_id, path)
64 except KeyError:
65 return None
66 return File(self.store, mode, sha, path, commit_id)
67
68 def update_file(self, path, content, user, parent_revision,
69 commit_message=None):
70 """The `user` is updating the file at `path` with `content`."""
71 commit_id, root_id = self._get_root()
72 if root_id is None:
73 root_tree = Tree()
74 else:
75 root_tree = self.store[root_id]
76 # Find all tree objects involved
77 tree = root_tree
78 trees = [root_tree]
79 elements = path.strip(posixpath.sep).split(posixpath.sep)
80 for el in elements[:-1]:
81 try:
82 (mode, sha) = tree[el]
83 except KeyError:
84 tree = Tree()
85 else:
86 if not stat.S_ISDIR(mode):
87 raise FileExists(
88 "File %s exists and is not a directory" % el)
89 tree = self.store[sha]
90 trees.append(tree)
91 if elements[-1] in tree:
92 (old_mode, old_sha) = tree[elements[-1]]
93 if stat.S_ISDIR(old_mode):
94 raise FileExists("File %s exists and is a directory" % path)
95 if old_sha != parent_revision and parent_revision is not None:
96 raise UpdateConflicts("File conflict %s != %s" % (old_sha,
97 parent_revision), old_sha)
98 blob = Blob.from_string(content.encode("utf-8"))
99 child = (stat.S_IFREG | 0644, blob.id)
100 self.store.add_object(blob)
101 assert len(trees) == len(elements)
102 for tree, name in zip(reversed(trees), reversed(elements)):
103 assert name != ""
104 tree[name] = child
105 self.store.add_object(tree)
106 child = (stat.S_IFDIR, tree.id)
107 if commit_message is None:
108 commit_message = ""
109 self.repo.do_commit(ref=self.ref, message=commit_message, author=user,
110 tree=tree.id)
111
112 def list_directory(self, directory_path):
113 """Return a list of File objects for in the directory path.
114
115 If the path doesn't exist, returns None. If the path exists but is
116 empty, an empty list is returned. Otherwise a list of File objects in
117 that directory.
118 """
119 if directory_path is None:
120 directory_path = ''
121 else:
122 directory_path = directory_path.strip(posixpath.sep)
123 commit_id, root_id = self._get_root()
124 if directory_path == '':
125 sha = root_id
126 mode = stat.S_IFDIR
127 else:
128 if root_id is None:
129 return None
130 try:
131 (mode, sha) = tree_lookup_path(self.store.__getitem__,
132 root_id, directory_path)
133 except KeyError:
134 return None
135 if mode is not None and stat.S_ISDIR(mode):
136 ret = []
137 for (name, mode, sha) in self.store[sha].iteritems():
138 ret.append(
139 File(self.store, mode, sha, posixpath.join(directory_path, name), commit_id))
140 return ret
141 else:
142 return None
143
144
145class File(object):
146 """A Git file object."""
147
148 implements(IFile)
149
150 def __init__(self, store, mode, sha, path, commit_sha):
151 self.store = store
152 self.mode = mode
153 self.sha = sha
154 self.path = path
155 self.commit_sha = commit_sha
156 self.base_name = posixpath.basename(path)
157 self.mimetype = mimetypes.guess_type(self.base_name)[0]
158
159 @property
160 def file_type(self):
161 """Work out the filetype based on the mimetype if possible."""
162 if self._is_directory:
163 return FileType.DIRECTORY
164 else:
165 if self.mimetype is None:
166 binary = self._is_binary
167 else:
168 binary = not self.mimetype.startswith('text/')
169 if binary:
170 return FileType.BINARY_FILE
171 else:
172 return FileType.TEXT_FILE
173
174 def get_content(self):
175 o = self.store[self.sha]
176 if isinstance(o, Blob):
177 return o.data
178 else:
179 return None
180
181 @property
182 def _is_directory(self):
183 return stat.S_ISDIR(self.mode)
184
185 @property
186 def _is_binary(self):
187 return '\0' in self.get_content()
188
189 def _get_last_modified_commit(self):
190 walker = Walker(self.store, include=[self.commit_sha],
191 paths=[self.path])
192 return iter(walker).next().commit
193
194 @property
195 def last_modified_in_revision(self):
196 return self.sha
197
198 @property
199 def last_modified_by(self):
200 return self._get_last_modified_commit().author
201
202 @property
203 def last_modified_date(self):
204 c = self._get_last_modified_commit()
205 return datetime.datetime.utcfromtimestamp(c.author_time)
0206
=== modified file 'wikkid/filestore/volatile.py'
--- wikkid/filestore/volatile.py 2010-06-07 08:56:13 +0000
+++ wikkid/filestore/volatile.py 2014-01-16 00:05:37 +0000
@@ -113,7 +113,8 @@
113 implements(IFile)113 implements(IFile)
114114
115 def __init__(self, path, content, file_id, user):115 def __init__(self, path, content, file_id, user):
116 BaseFile.__init__(self, path, file_id)116 BaseFile.__init__(self, path)
117 self.file_id = file_id
117 self.content = content118 self.content = content
118 self.last_modified_in_revision = None119 self.last_modified_in_revision = None
119 self.last_modified_by = user120 self.last_modified_by = user
120121
=== modified file 'wikkid/interface/filestore.py'
--- wikkid/interface/filestore.py 2010-06-07 08:24:06 +0000
+++ wikkid/interface/filestore.py 2014-01-16 00:05:37 +0000
@@ -70,9 +70,6 @@
7070
71 base_name = Attribute("The last part of the path.")71 base_name = Attribute("The last part of the path.")
7272
73 file_id = Attribute(
74 "The unique identifier for the file in the filestore.")
75
76 file_type = Attribute("Soon to be a Choice with a lazr.enum.")73 file_type = Attribute("Soon to be a Choice with a lazr.enum.")
7774
78 mimetype = Attribute(75 mimetype = Attribute(
7976
=== modified file 'wikkid/tests/__init__.py'
--- wikkid/tests/__init__.py 2012-06-14 08:40:53 +0000
+++ wikkid/tests/__init__.py 2014-01-16 00:05:37 +0000
@@ -41,6 +41,7 @@
41 'volatile_filestore',41 'volatile_filestore',
42 'bzr_filestore',42 'bzr_filestore',
43 'bzr_user',43 'bzr_user',
44 'git_filestore',
44 'view_dispatcher',45 'view_dispatcher',
45 'model',46 'model',
46 ]47 ]
4748
=== modified file 'wikkid/tests/filestore.py'
--- wikkid/tests/filestore.py 2010-11-08 00:00:06 +0000
+++ wikkid/tests/filestore.py 2014-01-16 00:05:37 +0000
@@ -42,6 +42,7 @@
42 filestore = self.make_filestore(42 filestore = self.make_filestore(
43 [('README', 'Content'),43 [('README', 'Content'),
44 ('lib/', None),44 ('lib/', None),
45 ('lib/foo', 'dummy data'),
45 ('image.jpg', 'pretend image'),46 ('image.jpg', 'pretend image'),
46 ('binary-file', 'a\0binary\0file'),47 ('binary-file', 'a\0binary\0file'),
47 ('simple.txt', 'A text file'),48 ('simple.txt', 'A text file'),
@@ -57,6 +58,7 @@
57 filestore = self.make_filestore(58 filestore = self.make_filestore(
58 [('README', 'Content'),59 [('README', 'Content'),
59 ('lib/', None),60 ('lib/', None),
61 ('lib/data', 'dummy data'),
60 ('image.jpg', 'pretend image'),62 ('image.jpg', 'pretend image'),
61 ('binary-file', 'a\0binary\0file'),63 ('binary-file', 'a\0binary\0file'),
62 ('simple.txt', 'A text file'),64 ('simple.txt', 'A text file'),
6365
=== added file 'wikkid/tests/test_git_filestore.py'
--- wikkid/tests/test_git_filestore.py 1970-01-01 00:00:00 +0000
+++ wikkid/tests/test_git_filestore.py 2014-01-16 00:05:37 +0000
@@ -0,0 +1,55 @@
1#
2# Copyright (C) 2012 Wikkid Developers.
3#
4# This software is licensed under the GNU Affero General Public License
5# version 3 (see the file LICENSE).
6
7"""Tests for the wikkid.filestore.git.FileStore."""
8
9from dulwich.repo import MemoryRepo
10
11from textwrap import dedent
12
13from wikkid.errors import UpdateConflicts
14from wikkid.filestore.git import (
15 FileStore,
16 )
17from wikkid.tests import ProvidesMixin, TestCase
18from wikkid.tests.filestore import TestFileStore
19
20
21class TestGitFileStore(TestCase, ProvidesMixin, TestFileStore):
22 """Tests for the git filestore and files."""
23
24 def make_filestore(self, contents=None):
25 repo = MemoryRepo()
26 fs = FileStore(repo)
27 if contents:
28 for (path, contents) in contents:
29 if contents is None:
30 # Directory
31 continue
32 fs.update_file(path, contents,
33 user="Somebody <test@example.com>",
34 parent_revision=None,
35 commit_message="Added by make_filestore")
36 return fs
37
38 def test_empty(self):
39 # Empty files do not have line endings, but they can be saved
40 # nonetheless.
41 filestore = self.make_filestore(
42 [('test.txt', 'several\nlines\nof\ncontent')])
43 f = filestore.get_file('test.txt')
44 base_rev = f.last_modified_in_revision
45 filestore.update_file(
46 'test.txt', '', 'Test Author <test@example.com>', base_rev)
47 curr = filestore.get_file('test.txt')
48 self.assertEqual('', curr.get_content())
49
50 def test_listing_directory_empty(self):
51 filestore = self.make_filestore(
52 [('empty/', None),
53 ])
54 listing = filestore.list_directory('empty')
55 self.assertIs(None, listing)
056
=== added file 'wikkid/user/git.py'
--- wikkid/user/git.py 1970-01-01 00:00:00 +0000
+++ wikkid/user/git.py 2014-01-16 00:05:37 +0000
@@ -0,0 +1,74 @@
1#
2# Copyright (C) 2010 Wikkid Developers
3#
4# This software is licensed under the GNU Affero General Public License
5# version 3 (see the file LICENSE).
6
7"""A user factory and user class which uses the git identity from the
8local Git config."""
9
10import email
11import logging
12
13from webob import Request
14from zope.interface import implements
15
16from wikkid.interface.user import IUser, IUserFactory
17from wikkid.user.baseuser import BaseUser
18
19
20def create_git_user_from_author_string(author):
21 name, address = email.Utils.parseaddr(author)
22 if name:
23 display_name = name
24 else:
25 display_name = address
26 return User(address, display_name, author)
27
28
29class LocalGitUserMiddleware(object):
30 """A middleware to inject a user into the environment."""
31
32 def __init__(self, app, repo):
33 self.app = app
34 config = repo.get_config_stack()
35 email = config.get(("user", ), "email")
36 name = config.get(("user", ), "name")
37 self.user = User(email, name, "%s <%s>" % (name, email))
38
39 def __call__(self, environ, start_response):
40 environ['wikkid.user'] = self.user
41 req = Request(environ)
42 resp = req.get_response(self.app)
43 return resp(environ, start_response)
44
45
46class UserFactory(object):
47 """Generate a user from local bazaar config."""
48
49 implements(IUserFactory)
50
51 def __init__(self, branch):
52 """Use the user config from the branch."""
53 config = branch.get_config()
54 self.user = create_git_user_from_author_string(config.username())
55 logger = logging.getLogger('wikkid')
56 logger.info(
57 'Using git identity: "%s", "%s"',
58 self.user.display_name, self.user.email)
59
60 def create(self, request):
61 """Create a User."""
62 return self.user
63
64
65class User(BaseUser):
66 """A user from the local bazaar config."""
67
68 implements(IUser)
69
70 def __init__(self, email, display_name, committer_id):
71 self.email = email
72 self.display_name = display_name
73 self.committer_id = committer_id
74

Subscribers

People subscribed via source and target branches