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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey | Approve | ||
Gavin Panella | Approve | ||
Review via email: mp+183373@code.launchpad.net |
Commit message
Description of the change
Add a dulwich-based filestore backend to wikkid, which can be used for git repositories.
- 76. By Jelmer Vernooij
-
Simplify commit, add --format=git option.
- 77. By Jelmer Vernooij
-
Fix user id.
Jelmer Vernooij (jelmer) wrote : | # |
Jelmer Vernooij (jelmer) wrote : | # |
ping (-:
Jelmer Vernooij (jelmer) wrote : | # |
ping :-)
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.
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 :)
Jelmer Vernooij (jelmer) wrote : | # |
Thanks for the review, Gavin. Much appreciated! :-)
I'll follow up with some fixes when I get back from vacation.
- 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.
Jelmer Vernooij (jelmer) wrote : | # |
Thanks, I've now fixed the issues you pointed out.
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 :-)
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. :-)
Jelmer Vernooij (jelmer) wrote : | # |
ping? :)
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.
Jelmer Vernooij (jelmer) wrote : | # |
Thanks, Tim! Merged.
Preview Diff
1 | === modified file 'bin/wikkid-serve' | |||
2 | --- bin/wikkid-serve 2012-05-17 09:32:49 +0000 | |||
3 | +++ bin/wikkid-serve 2014-01-16 00:05:37 +0000 | |||
4 | @@ -18,8 +18,6 @@ | |||
5 | 18 | import optparse | 18 | import optparse |
6 | 19 | import sys | 19 | import sys |
7 | 20 | 20 | ||
8 | 21 | from bzrlib.workingtree import WorkingTree | ||
9 | 22 | |||
10 | 23 | from wikkid import version | 21 | from wikkid import version |
11 | 24 | from wikkid.app import WikkidApp | 22 | from wikkid.app import WikkidApp |
12 | 25 | from wikkid.context import ( | 23 | from wikkid.context import ( |
13 | @@ -28,8 +26,6 @@ | |||
14 | 28 | DEFAULT_PORT, | 26 | DEFAULT_PORT, |
15 | 29 | ExecutionContext, | 27 | ExecutionContext, |
16 | 30 | ) | 28 | ) |
17 | 31 | from wikkid.filestore.bzr import FileStore | ||
18 | 32 | from wikkid.user.bzr import LocalBazaarUserMiddleware | ||
19 | 33 | 29 | ||
20 | 34 | 30 | ||
21 | 35 | def setup_logging(): | 31 | def setup_logging(): |
22 | @@ -48,6 +44,8 @@ | |||
23 | 48 | usage = "Usage: %prog [options] <wiki-branch>" | 44 | usage = "Usage: %prog [options] <wiki-branch>" |
24 | 49 | parser = optparse.OptionParser( | 45 | parser = optparse.OptionParser( |
25 | 50 | usage=usage, description="Run a Wikkid Wiki server.", version=version) | 46 | usage=usage, description="Run a Wikkid Wiki server.", version=version) |
26 | 47 | parser.add_option('--format', type='choice', default='bzr', | ||
27 | 48 | choices=['bzr', 'git'], help=("Default repository format to use.")) | ||
28 | 51 | parser.add_option( | 49 | parser.add_option( |
29 | 52 | '--host', type='string', default=DEFAULT_HOST, | 50 | '--host', type='string', default=DEFAULT_HOST, |
30 | 53 | help=('The interface to listen on. Defaults to %r' % DEFAULT_HOST)) | 51 | help=('The interface to listen on. Defaults to %r' % DEFAULT_HOST)) |
31 | @@ -82,12 +80,24 @@ | |||
32 | 82 | logger = logging.getLogger('wikkid') | 80 | logger = logging.getLogger('wikkid') |
33 | 83 | logger.setLevel(logging.INFO) | 81 | logger.setLevel(logging.INFO) |
34 | 84 | 82 | ||
38 | 85 | working_tree = WorkingTree.open(branch) | 83 | if options.format == 'bzr': |
39 | 86 | logger.info('Using: %s', working_tree) | 84 | from bzrlib.workingtree import WorkingTree |
40 | 87 | filestore = FileStore(working_tree) | 85 | from wikkid.filestore.bzr import FileStore |
41 | 86 | from wikkid.user.bzr import LocalBazaarUserMiddleware | ||
42 | 87 | |||
43 | 88 | working_tree = WorkingTree.open(branch) | ||
44 | 89 | logger.info('Using: %s', working_tree) | ||
45 | 90 | filestore = FileStore(working_tree) | ||
46 | 91 | elif options.format == 'git': | ||
47 | 92 | from wikkid.filestore.git import FileStore | ||
48 | 93 | from wikkid.user.git import LocalGitUserMiddleware | ||
49 | 94 | filestore = FileStore.from_path(branch) | ||
50 | 88 | 95 | ||
51 | 89 | app = WikkidApp(filestore=filestore, execution_context=execution_context) | 96 | app = WikkidApp(filestore=filestore, execution_context=execution_context) |
53 | 90 | app = LocalBazaarUserMiddleware(app, working_tree.branch) | 97 | if options.format == 'bzr': |
54 | 98 | app = LocalBazaarUserMiddleware(app, working_tree.branch) | ||
55 | 99 | elif options.format == 'git': | ||
56 | 100 | app = LocalGitUserMiddleware(app, filestore.repo) | ||
57 | 91 | from wsgiref.simple_server import make_server | 101 | from wsgiref.simple_server import make_server |
58 | 92 | httpd = make_server(options.host, options.port, app) | 102 | httpd = make_server(options.host, options.port, app) |
59 | 93 | logger.info('Serving on http://%s:%s', options.host, options.port) | 103 | logger.info('Serving on http://%s:%s', options.host, options.port) |
60 | 94 | 104 | ||
61 | === modified file 'setup.py' | |||
62 | --- setup.py 2012-05-17 10:28:31 +0000 | |||
63 | +++ setup.py 2014-01-16 00:05:37 +0000 | |||
64 | @@ -24,6 +24,7 @@ | |||
65 | 24 | include_package_data=True, | 24 | include_package_data=True, |
66 | 25 | install_requires=[ | 25 | install_requires=[ |
67 | 26 | 'docutils', | 26 | 'docutils', |
68 | 27 | 'dulwich', | ||
69 | 27 | 'jinja2', | 28 | 'jinja2', |
70 | 28 | 'pygments', | 29 | 'pygments', |
71 | 29 | 'twisted', | 30 | 'twisted', |
72 | 30 | 31 | ||
73 | === modified file 'wikkid/dispatcher.py' | |||
74 | --- wikkid/dispatcher.py 2010-11-12 09:00:42 +0000 | |||
75 | +++ wikkid/dispatcher.py 2014-01-16 00:05:37 +0000 | |||
76 | @@ -53,7 +53,7 @@ | |||
77 | 53 | # Don't register. | 53 | # Don't register. |
78 | 54 | return | 54 | return |
79 | 55 | key = (interface, view_name) | 55 | key = (interface, view_name) |
81 | 56 | assert key not in _VIEW_REGISTRY, "key already registered: %r" % key | 56 | assert key not in _VIEW_REGISTRY, "key already registered: %r" % (key,) |
82 | 57 | _VIEW_REGISTRY[key] = view_class | 57 | _VIEW_REGISTRY[key] = view_class |
83 | 58 | if default_view: | 58 | if default_view: |
84 | 59 | _VIEW_REGISTRY[(interface, None)] = view_class | 59 | _VIEW_REGISTRY[(interface, None)] = view_class |
85 | 60 | 60 | ||
86 | === modified file 'wikkid/filestore/basefile.py' | |||
87 | --- wikkid/filestore/basefile.py 2010-05-12 10:39:13 +0000 | |||
88 | +++ wikkid/filestore/basefile.py 2014-01-16 00:05:37 +0000 | |||
89 | @@ -16,9 +16,8 @@ | |||
90 | 16 | class BaseFile(object): | 16 | class BaseFile(object): |
91 | 17 | """Provide common fields and methods and properties for files.""" | 17 | """Provide common fields and methods and properties for files.""" |
92 | 18 | 18 | ||
94 | 19 | def __init__(self, path, file_id): | 19 | def __init__(self, path): |
95 | 20 | self.path = path | 20 | self.path = path |
96 | 21 | self.file_id = file_id | ||
97 | 22 | self.base_name = urlutils.basename(path) | 21 | self.base_name = urlutils.basename(path) |
98 | 23 | self._mimetype = mimetypes.guess_type(self.base_name)[0] | 22 | self._mimetype = mimetypes.guess_type(self.base_name)[0] |
99 | 24 | 23 | ||
100 | 25 | 24 | ||
101 | === modified file 'wikkid/filestore/bzr.py' | |||
102 | --- wikkid/filestore/bzr.py 2012-06-14 01:14:00 +0000 | |||
103 | +++ wikkid/filestore/bzr.py 2014-01-16 00:05:37 +0000 | |||
104 | @@ -243,7 +243,8 @@ | |||
105 | 243 | implements(IFile) | 243 | implements(IFile) |
106 | 244 | 244 | ||
107 | 245 | def __init__(self, filestore, path, file_id): | 245 | def __init__(self, filestore, path, file_id): |
109 | 246 | BaseFile.__init__(self, path, file_id) | 246 | BaseFile.__init__(self, path) |
110 | 247 | self.file_id = file_id | ||
111 | 247 | self.filestore = filestore | 248 | self.filestore = filestore |
112 | 248 | # This isn't entirely necessary. | 249 | # This isn't entirely necessary. |
113 | 249 | self.tree = self.filestore.tree | 250 | self.tree = self.filestore.tree |
114 | 250 | 251 | ||
115 | === added file 'wikkid/filestore/git.py' | |||
116 | --- wikkid/filestore/git.py 1970-01-01 00:00:00 +0000 | |||
117 | +++ wikkid/filestore/git.py 2014-01-16 00:05:37 +0000 | |||
118 | @@ -0,0 +1,205 @@ | |||
119 | 1 | # | ||
120 | 2 | # Copyright (C) 2012 Wikkid Developers. | ||
121 | 3 | # | ||
122 | 4 | # This software is licensed under the GNU Affero General Public License | ||
123 | 5 | # version 3 (see the file LICENSE). | ||
124 | 6 | |||
125 | 7 | """A git filestore using Dulwich. | ||
126 | 8 | |||
127 | 9 | """ | ||
128 | 10 | |||
129 | 11 | import datetime | ||
130 | 12 | import mimetypes | ||
131 | 13 | |||
132 | 14 | from dulwich.objects import Blob, Tree, ZERO_SHA | ||
133 | 15 | from dulwich.object_store import tree_lookup_path | ||
134 | 16 | from dulwich.repo import Repo | ||
135 | 17 | from dulwich.walk import Walker | ||
136 | 18 | import posixpath | ||
137 | 19 | import stat | ||
138 | 20 | |||
139 | 21 | from zope.interface import implements | ||
140 | 22 | |||
141 | 23 | from wikkid.errors import FileExists, UpdateConflicts | ||
142 | 24 | from wikkid.interface.filestore import FileType, IFile, IFileStore | ||
143 | 25 | |||
144 | 26 | |||
145 | 27 | class FileStore(object): | ||
146 | 28 | """A filestore that just uses an internal map to store data.""" | ||
147 | 29 | |||
148 | 30 | implements(IFileStore) | ||
149 | 31 | |||
150 | 32 | @classmethod | ||
151 | 33 | def from_path(cls, path): | ||
152 | 34 | return cls(Repo(path)) | ||
153 | 35 | |||
154 | 36 | def __init__(self, repo, ref='HEAD'): | ||
155 | 37 | """Repo is a dulwich repository.""" | ||
156 | 38 | self.repo = repo | ||
157 | 39 | self.ref = ref | ||
158 | 40 | |||
159 | 41 | @property | ||
160 | 42 | def store(self): | ||
161 | 43 | return self.repo.object_store | ||
162 | 44 | |||
163 | 45 | def _get_root(self, revision=None): | ||
164 | 46 | if revision is None: | ||
165 | 47 | try: | ||
166 | 48 | revision = self.repo.refs[self.ref] | ||
167 | 49 | except KeyError: | ||
168 | 50 | revision = ZERO_SHA | ||
169 | 51 | try: | ||
170 | 52 | return (revision, self.repo[revision].tree) | ||
171 | 53 | except KeyError: | ||
172 | 54 | return None, None | ||
173 | 55 | |||
174 | 56 | def get_file(self, path): | ||
175 | 57 | """Return an object representing the file.""" | ||
176 | 58 | commit_id, root_id = self._get_root() | ||
177 | 59 | if root_id is None: | ||
178 | 60 | return None | ||
179 | 61 | try: | ||
180 | 62 | (mode, sha) = tree_lookup_path(self.store.__getitem__, | ||
181 | 63 | root_id, path) | ||
182 | 64 | except KeyError: | ||
183 | 65 | return None | ||
184 | 66 | return File(self.store, mode, sha, path, commit_id) | ||
185 | 67 | |||
186 | 68 | def update_file(self, path, content, user, parent_revision, | ||
187 | 69 | commit_message=None): | ||
188 | 70 | """The `user` is updating the file at `path` with `content`.""" | ||
189 | 71 | commit_id, root_id = self._get_root() | ||
190 | 72 | if root_id is None: | ||
191 | 73 | root_tree = Tree() | ||
192 | 74 | else: | ||
193 | 75 | root_tree = self.store[root_id] | ||
194 | 76 | # Find all tree objects involved | ||
195 | 77 | tree = root_tree | ||
196 | 78 | trees = [root_tree] | ||
197 | 79 | elements = path.strip(posixpath.sep).split(posixpath.sep) | ||
198 | 80 | for el in elements[:-1]: | ||
199 | 81 | try: | ||
200 | 82 | (mode, sha) = tree[el] | ||
201 | 83 | except KeyError: | ||
202 | 84 | tree = Tree() | ||
203 | 85 | else: | ||
204 | 86 | if not stat.S_ISDIR(mode): | ||
205 | 87 | raise FileExists( | ||
206 | 88 | "File %s exists and is not a directory" % el) | ||
207 | 89 | tree = self.store[sha] | ||
208 | 90 | trees.append(tree) | ||
209 | 91 | if elements[-1] in tree: | ||
210 | 92 | (old_mode, old_sha) = tree[elements[-1]] | ||
211 | 93 | if stat.S_ISDIR(old_mode): | ||
212 | 94 | raise FileExists("File %s exists and is a directory" % path) | ||
213 | 95 | if old_sha != parent_revision and parent_revision is not None: | ||
214 | 96 | raise UpdateConflicts("File conflict %s != %s" % (old_sha, | ||
215 | 97 | parent_revision), old_sha) | ||
216 | 98 | blob = Blob.from_string(content.encode("utf-8")) | ||
217 | 99 | child = (stat.S_IFREG | 0644, blob.id) | ||
218 | 100 | self.store.add_object(blob) | ||
219 | 101 | assert len(trees) == len(elements) | ||
220 | 102 | for tree, name in zip(reversed(trees), reversed(elements)): | ||
221 | 103 | assert name != "" | ||
222 | 104 | tree[name] = child | ||
223 | 105 | self.store.add_object(tree) | ||
224 | 106 | child = (stat.S_IFDIR, tree.id) | ||
225 | 107 | if commit_message is None: | ||
226 | 108 | commit_message = "" | ||
227 | 109 | self.repo.do_commit(ref=self.ref, message=commit_message, author=user, | ||
228 | 110 | tree=tree.id) | ||
229 | 111 | |||
230 | 112 | def list_directory(self, directory_path): | ||
231 | 113 | """Return a list of File objects for in the directory path. | ||
232 | 114 | |||
233 | 115 | If the path doesn't exist, returns None. If the path exists but is | ||
234 | 116 | empty, an empty list is returned. Otherwise a list of File objects in | ||
235 | 117 | that directory. | ||
236 | 118 | """ | ||
237 | 119 | if directory_path is None: | ||
238 | 120 | directory_path = '' | ||
239 | 121 | else: | ||
240 | 122 | directory_path = directory_path.strip(posixpath.sep) | ||
241 | 123 | commit_id, root_id = self._get_root() | ||
242 | 124 | if directory_path == '': | ||
243 | 125 | sha = root_id | ||
244 | 126 | mode = stat.S_IFDIR | ||
245 | 127 | else: | ||
246 | 128 | if root_id is None: | ||
247 | 129 | return None | ||
248 | 130 | try: | ||
249 | 131 | (mode, sha) = tree_lookup_path(self.store.__getitem__, | ||
250 | 132 | root_id, directory_path) | ||
251 | 133 | except KeyError: | ||
252 | 134 | return None | ||
253 | 135 | if mode is not None and stat.S_ISDIR(mode): | ||
254 | 136 | ret = [] | ||
255 | 137 | for (name, mode, sha) in self.store[sha].iteritems(): | ||
256 | 138 | ret.append( | ||
257 | 139 | File(self.store, mode, sha, posixpath.join(directory_path, name), commit_id)) | ||
258 | 140 | return ret | ||
259 | 141 | else: | ||
260 | 142 | return None | ||
261 | 143 | |||
262 | 144 | |||
263 | 145 | class File(object): | ||
264 | 146 | """A Git file object.""" | ||
265 | 147 | |||
266 | 148 | implements(IFile) | ||
267 | 149 | |||
268 | 150 | def __init__(self, store, mode, sha, path, commit_sha): | ||
269 | 151 | self.store = store | ||
270 | 152 | self.mode = mode | ||
271 | 153 | self.sha = sha | ||
272 | 154 | self.path = path | ||
273 | 155 | self.commit_sha = commit_sha | ||
274 | 156 | self.base_name = posixpath.basename(path) | ||
275 | 157 | self.mimetype = mimetypes.guess_type(self.base_name)[0] | ||
276 | 158 | |||
277 | 159 | @property | ||
278 | 160 | def file_type(self): | ||
279 | 161 | """Work out the filetype based on the mimetype if possible.""" | ||
280 | 162 | if self._is_directory: | ||
281 | 163 | return FileType.DIRECTORY | ||
282 | 164 | else: | ||
283 | 165 | if self.mimetype is None: | ||
284 | 166 | binary = self._is_binary | ||
285 | 167 | else: | ||
286 | 168 | binary = not self.mimetype.startswith('text/') | ||
287 | 169 | if binary: | ||
288 | 170 | return FileType.BINARY_FILE | ||
289 | 171 | else: | ||
290 | 172 | return FileType.TEXT_FILE | ||
291 | 173 | |||
292 | 174 | def get_content(self): | ||
293 | 175 | o = self.store[self.sha] | ||
294 | 176 | if isinstance(o, Blob): | ||
295 | 177 | return o.data | ||
296 | 178 | else: | ||
297 | 179 | return None | ||
298 | 180 | |||
299 | 181 | @property | ||
300 | 182 | def _is_directory(self): | ||
301 | 183 | return stat.S_ISDIR(self.mode) | ||
302 | 184 | |||
303 | 185 | @property | ||
304 | 186 | def _is_binary(self): | ||
305 | 187 | return '\0' in self.get_content() | ||
306 | 188 | |||
307 | 189 | def _get_last_modified_commit(self): | ||
308 | 190 | walker = Walker(self.store, include=[self.commit_sha], | ||
309 | 191 | paths=[self.path]) | ||
310 | 192 | return iter(walker).next().commit | ||
311 | 193 | |||
312 | 194 | @property | ||
313 | 195 | def last_modified_in_revision(self): | ||
314 | 196 | return self.sha | ||
315 | 197 | |||
316 | 198 | @property | ||
317 | 199 | def last_modified_by(self): | ||
318 | 200 | return self._get_last_modified_commit().author | ||
319 | 201 | |||
320 | 202 | @property | ||
321 | 203 | def last_modified_date(self): | ||
322 | 204 | c = self._get_last_modified_commit() | ||
323 | 205 | return datetime.datetime.utcfromtimestamp(c.author_time) | ||
324 | 0 | 206 | ||
325 | === modified file 'wikkid/filestore/volatile.py' | |||
326 | --- wikkid/filestore/volatile.py 2010-06-07 08:56:13 +0000 | |||
327 | +++ wikkid/filestore/volatile.py 2014-01-16 00:05:37 +0000 | |||
328 | @@ -113,7 +113,8 @@ | |||
329 | 113 | implements(IFile) | 113 | implements(IFile) |
330 | 114 | 114 | ||
331 | 115 | def __init__(self, path, content, file_id, user): | 115 | def __init__(self, path, content, file_id, user): |
333 | 116 | BaseFile.__init__(self, path, file_id) | 116 | BaseFile.__init__(self, path) |
334 | 117 | self.file_id = file_id | ||
335 | 117 | self.content = content | 118 | self.content = content |
336 | 118 | self.last_modified_in_revision = None | 119 | self.last_modified_in_revision = None |
337 | 119 | self.last_modified_by = user | 120 | self.last_modified_by = user |
338 | 120 | 121 | ||
339 | === modified file 'wikkid/interface/filestore.py' | |||
340 | --- wikkid/interface/filestore.py 2010-06-07 08:24:06 +0000 | |||
341 | +++ wikkid/interface/filestore.py 2014-01-16 00:05:37 +0000 | |||
342 | @@ -70,9 +70,6 @@ | |||
343 | 70 | 70 | ||
344 | 71 | base_name = Attribute("The last part of the path.") | 71 | base_name = Attribute("The last part of the path.") |
345 | 72 | 72 | ||
346 | 73 | file_id = Attribute( | ||
347 | 74 | "The unique identifier for the file in the filestore.") | ||
348 | 75 | |||
349 | 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.") |
350 | 77 | 74 | ||
351 | 78 | mimetype = Attribute( | 75 | mimetype = Attribute( |
352 | 79 | 76 | ||
353 | === modified file 'wikkid/tests/__init__.py' | |||
354 | --- wikkid/tests/__init__.py 2012-06-14 08:40:53 +0000 | |||
355 | +++ wikkid/tests/__init__.py 2014-01-16 00:05:37 +0000 | |||
356 | @@ -41,6 +41,7 @@ | |||
357 | 41 | 'volatile_filestore', | 41 | 'volatile_filestore', |
358 | 42 | 'bzr_filestore', | 42 | 'bzr_filestore', |
359 | 43 | 'bzr_user', | 43 | 'bzr_user', |
360 | 44 | 'git_filestore', | ||
361 | 44 | 'view_dispatcher', | 45 | 'view_dispatcher', |
362 | 45 | 'model', | 46 | 'model', |
363 | 46 | ] | 47 | ] |
364 | 47 | 48 | ||
365 | === modified file 'wikkid/tests/filestore.py' | |||
366 | --- wikkid/tests/filestore.py 2010-11-08 00:00:06 +0000 | |||
367 | +++ wikkid/tests/filestore.py 2014-01-16 00:05:37 +0000 | |||
368 | @@ -42,6 +42,7 @@ | |||
369 | 42 | filestore = self.make_filestore( | 42 | filestore = self.make_filestore( |
370 | 43 | [('README', 'Content'), | 43 | [('README', 'Content'), |
371 | 44 | ('lib/', None), | 44 | ('lib/', None), |
372 | 45 | ('lib/foo', 'dummy data'), | ||
373 | 45 | ('image.jpg', 'pretend image'), | 46 | ('image.jpg', 'pretend image'), |
374 | 46 | ('binary-file', 'a\0binary\0file'), | 47 | ('binary-file', 'a\0binary\0file'), |
375 | 47 | ('simple.txt', 'A text file'), | 48 | ('simple.txt', 'A text file'), |
376 | @@ -57,6 +58,7 @@ | |||
377 | 57 | filestore = self.make_filestore( | 58 | filestore = self.make_filestore( |
378 | 58 | [('README', 'Content'), | 59 | [('README', 'Content'), |
379 | 59 | ('lib/', None), | 60 | ('lib/', None), |
380 | 61 | ('lib/data', 'dummy data'), | ||
381 | 60 | ('image.jpg', 'pretend image'), | 62 | ('image.jpg', 'pretend image'), |
382 | 61 | ('binary-file', 'a\0binary\0file'), | 63 | ('binary-file', 'a\0binary\0file'), |
383 | 62 | ('simple.txt', 'A text file'), | 64 | ('simple.txt', 'A text file'), |
384 | 63 | 65 | ||
385 | === added file 'wikkid/tests/test_git_filestore.py' | |||
386 | --- wikkid/tests/test_git_filestore.py 1970-01-01 00:00:00 +0000 | |||
387 | +++ wikkid/tests/test_git_filestore.py 2014-01-16 00:05:37 +0000 | |||
388 | @@ -0,0 +1,55 @@ | |||
389 | 1 | # | ||
390 | 2 | # Copyright (C) 2012 Wikkid Developers. | ||
391 | 3 | # | ||
392 | 4 | # This software is licensed under the GNU Affero General Public License | ||
393 | 5 | # version 3 (see the file LICENSE). | ||
394 | 6 | |||
395 | 7 | """Tests for the wikkid.filestore.git.FileStore.""" | ||
396 | 8 | |||
397 | 9 | from dulwich.repo import MemoryRepo | ||
398 | 10 | |||
399 | 11 | from textwrap import dedent | ||
400 | 12 | |||
401 | 13 | from wikkid.errors import UpdateConflicts | ||
402 | 14 | from wikkid.filestore.git import ( | ||
403 | 15 | FileStore, | ||
404 | 16 | ) | ||
405 | 17 | from wikkid.tests import ProvidesMixin, TestCase | ||
406 | 18 | from wikkid.tests.filestore import TestFileStore | ||
407 | 19 | |||
408 | 20 | |||
409 | 21 | class TestGitFileStore(TestCase, ProvidesMixin, TestFileStore): | ||
410 | 22 | """Tests for the git filestore and files.""" | ||
411 | 23 | |||
412 | 24 | def make_filestore(self, contents=None): | ||
413 | 25 | repo = MemoryRepo() | ||
414 | 26 | fs = FileStore(repo) | ||
415 | 27 | if contents: | ||
416 | 28 | for (path, contents) in contents: | ||
417 | 29 | if contents is None: | ||
418 | 30 | # Directory | ||
419 | 31 | continue | ||
420 | 32 | fs.update_file(path, contents, | ||
421 | 33 | user="Somebody <test@example.com>", | ||
422 | 34 | parent_revision=None, | ||
423 | 35 | commit_message="Added by make_filestore") | ||
424 | 36 | return fs | ||
425 | 37 | |||
426 | 38 | def test_empty(self): | ||
427 | 39 | # Empty files do not have line endings, but they can be saved | ||
428 | 40 | # nonetheless. | ||
429 | 41 | filestore = self.make_filestore( | ||
430 | 42 | [('test.txt', 'several\nlines\nof\ncontent')]) | ||
431 | 43 | f = filestore.get_file('test.txt') | ||
432 | 44 | base_rev = f.last_modified_in_revision | ||
433 | 45 | filestore.update_file( | ||
434 | 46 | 'test.txt', '', 'Test Author <test@example.com>', base_rev) | ||
435 | 47 | curr = filestore.get_file('test.txt') | ||
436 | 48 | self.assertEqual('', curr.get_content()) | ||
437 | 49 | |||
438 | 50 | def test_listing_directory_empty(self): | ||
439 | 51 | filestore = self.make_filestore( | ||
440 | 52 | [('empty/', None), | ||
441 | 53 | ]) | ||
442 | 54 | listing = filestore.list_directory('empty') | ||
443 | 55 | self.assertIs(None, listing) | ||
444 | 0 | 56 | ||
445 | === added file 'wikkid/user/git.py' | |||
446 | --- wikkid/user/git.py 1970-01-01 00:00:00 +0000 | |||
447 | +++ wikkid/user/git.py 2014-01-16 00:05:37 +0000 | |||
448 | @@ -0,0 +1,74 @@ | |||
449 | 1 | # | ||
450 | 2 | # Copyright (C) 2010 Wikkid Developers | ||
451 | 3 | # | ||
452 | 4 | # This software is licensed under the GNU Affero General Public License | ||
453 | 5 | # version 3 (see the file LICENSE). | ||
454 | 6 | |||
455 | 7 | """A user factory and user class which uses the git identity from the | ||
456 | 8 | local Git config.""" | ||
457 | 9 | |||
458 | 10 | import email | ||
459 | 11 | import logging | ||
460 | 12 | |||
461 | 13 | from webob import Request | ||
462 | 14 | from zope.interface import implements | ||
463 | 15 | |||
464 | 16 | from wikkid.interface.user import IUser, IUserFactory | ||
465 | 17 | from wikkid.user.baseuser import BaseUser | ||
466 | 18 | |||
467 | 19 | |||
468 | 20 | def create_git_user_from_author_string(author): | ||
469 | 21 | name, address = email.Utils.parseaddr(author) | ||
470 | 22 | if name: | ||
471 | 23 | display_name = name | ||
472 | 24 | else: | ||
473 | 25 | display_name = address | ||
474 | 26 | return User(address, display_name, author) | ||
475 | 27 | |||
476 | 28 | |||
477 | 29 | class LocalGitUserMiddleware(object): | ||
478 | 30 | """A middleware to inject a user into the environment.""" | ||
479 | 31 | |||
480 | 32 | def __init__(self, app, repo): | ||
481 | 33 | self.app = app | ||
482 | 34 | config = repo.get_config_stack() | ||
483 | 35 | email = config.get(("user", ), "email") | ||
484 | 36 | name = config.get(("user", ), "name") | ||
485 | 37 | self.user = User(email, name, "%s <%s>" % (name, email)) | ||
486 | 38 | |||
487 | 39 | def __call__(self, environ, start_response): | ||
488 | 40 | environ['wikkid.user'] = self.user | ||
489 | 41 | req = Request(environ) | ||
490 | 42 | resp = req.get_response(self.app) | ||
491 | 43 | return resp(environ, start_response) | ||
492 | 44 | |||
493 | 45 | |||
494 | 46 | class UserFactory(object): | ||
495 | 47 | """Generate a user from local bazaar config.""" | ||
496 | 48 | |||
497 | 49 | implements(IUserFactory) | ||
498 | 50 | |||
499 | 51 | def __init__(self, branch): | ||
500 | 52 | """Use the user config from the branch.""" | ||
501 | 53 | config = branch.get_config() | ||
502 | 54 | self.user = create_git_user_from_author_string(config.username()) | ||
503 | 55 | logger = logging.getLogger('wikkid') | ||
504 | 56 | logger.info( | ||
505 | 57 | 'Using git identity: "%s", "%s"', | ||
506 | 58 | self.user.display_name, self.user.email) | ||
507 | 59 | |||
508 | 60 | def create(self, request): | ||
509 | 61 | """Create a User.""" | ||
510 | 62 | return self.user | ||
511 | 63 | |||
512 | 64 | |||
513 | 65 | class User(BaseUser): | ||
514 | 66 | """A user from the local bazaar config.""" | ||
515 | 67 | |||
516 | 68 | implements(IUser) | ||
517 | 69 | |||
518 | 70 | def __init__(self, email, display_name, committer_id): | ||
519 | 71 | self.email = email | ||
520 | 72 | self.display_name = display_name | ||
521 | 73 | self.committer_id = committer_id | ||
522 | 74 |
ping, anyone?