Merge lp:~abentley/wikkid/branch-file-store into lp:wikkid
- branch-file-store
- Merge into trunk
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Merged at revision: | 67 |
Proposed branch: | lp:~abentley/wikkid/branch-file-store |
Merge into: | lp:wikkid |
Diff against target: |
375 lines (+167/-60) 2 files modified
wikkid/filestore/bzr.py (+152/-59) wikkid/tests/test_bzr_filestore.py (+15/-1) |
To merge this branch: | bzr merge lp:~abentley/wikkid/branch-file-store |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey | Approve | ||
Review via email: mp+66484@code.launchpad.net |
Commit message
Description of the change
A branch-backed file store. Certain things were refactored for reuse.
Commits are performed on the branch directly, by using a TransformPreview.
Win!
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'wikkid/filestore/bzr.py' |
2 | --- wikkid/filestore/bzr.py 2010-10-17 01:47:00 +0000 |
3 | +++ wikkid/filestore/bzr.py 2011-06-30 16:12:03 +0000 |
4 | @@ -11,11 +11,12 @@ |
5 | |
6 | from zope.interface import implements |
7 | |
8 | -from bzrlib.errors import BinaryFile |
9 | +from bzrlib.errors import BinaryFile, MalformedTransform |
10 | from bzrlib.merge3 import Merge3 |
11 | -from bzrlib.osutils import split_lines |
12 | +from bzrlib.osutils import splitpath, split_lines |
13 | from bzrlib.revision import NULL_REVISION |
14 | -from bzrlib.textfile import check_text_path |
15 | +from bzrlib.textfile import check_text_lines |
16 | +from bzrlib.transform import FinalPaths, TransformPreview |
17 | from bzrlib.urlutils import basename, dirname, joinpath |
18 | |
19 | from wikkid.errors import FileExists, UpdateConflicts |
20 | @@ -38,18 +39,61 @@ |
21 | return '\n' |
22 | |
23 | |
24 | +def get_commit_message(commit_message): |
25 | + if commit_message is None or commit_message.strip() == '': |
26 | + return 'No description of change given.' |
27 | + return commit_message |
28 | + |
29 | + |
30 | +def normalize_content(content): |
31 | + # Default to simple '\n' line endings. |
32 | + content = normalize_line_endings(content) |
33 | + # Make sure the content ends with a new-line. This makes |
34 | + # end of file conflicts nicer. |
35 | + if not content.endswith('\n'): |
36 | + content += '\n' |
37 | + return content |
38 | + |
39 | + |
40 | +def iter_paths(path): |
41 | + path_segments = splitpath(path) |
42 | + while len(path_segments) > 0: |
43 | + tail = path_segments.pop() |
44 | + if len(path_segments) == 0: |
45 | + yield '', tail |
46 | + else: |
47 | + yield joinpath(*path_segments), tail |
48 | + |
49 | + |
50 | +def create_parents(tt, path, trans_id): |
51 | + prev_trans_id = trans_id |
52 | + for parent_path, tail in iter_paths(path): |
53 | + trans_id = tt.trans_id_tree_path(parent_path) |
54 | + tt.adjust_path(tail, trans_id, prev_trans_id) |
55 | + if tt.tree_kind(trans_id) is not None: |
56 | + break |
57 | + tt.create_directory(trans_id) |
58 | + tt.version_file(trans_id, trans_id) |
59 | + prev_name = tail |
60 | + prev_trans_id = trans_id |
61 | + |
62 | + |
63 | class FileStore(object): |
64 | """Wraps a Bazaar branch to be a filestore.""" |
65 | |
66 | implements(IFileStore) |
67 | |
68 | - def __init__(self, working_tree): |
69 | - self.working_tree = working_tree |
70 | + def __init__(self, tree): |
71 | + self.tree = tree |
72 | + self.branch = tree.branch |
73 | self.logger = logging.getLogger('wikkid') |
74 | |
75 | + def basis_tree(self): |
76 | + return self.tree.basis_tree() |
77 | + |
78 | def get_file(self, path): |
79 | """Return an object representing the file at specified path.""" |
80 | - file_id = self.working_tree.path2id(path) |
81 | + file_id = self.tree.path2id(path) |
82 | if file_id is None: |
83 | return None |
84 | else: |
85 | @@ -62,16 +106,15 @@ |
86 | This is going to be really interesting when we need to deal with |
87 | conflicts. |
88 | """ |
89 | - if commit_message is None or commit_message.strip() == '': |
90 | - commit_message = 'No description of change given.' |
91 | + commit_message = get_commit_message(commit_message) |
92 | if parent_revision is None: |
93 | parent_revision = NULL_REVISION |
94 | # Firstly we want to lock the tree for writing. |
95 | - self.working_tree.lock_write() |
96 | + self.tree.lock_write() |
97 | try: |
98 | # Look to see if the path is there. If it is then we are doing an |
99 | # update. If it isn't we are doing an add. |
100 | - file_id = self.working_tree.path2id(path) |
101 | + file_id = self.tree.path2id(path) |
102 | if file_id is None: |
103 | self._add_file(path, content, author, commit_message) |
104 | else: |
105 | @@ -80,7 +123,7 @@ |
106 | file_id, path, content, author, parent_revision, |
107 | commit_message) |
108 | finally: |
109 | - self.working_tree.unlock() |
110 | + self.tree.unlock() |
111 | |
112 | def _ensure_directory_or_nonexistant(self, dir_path): |
113 | """Ensure the dir_path defines a directory or doesn't exist. |
114 | @@ -106,13 +149,8 @@ |
115 | |
116 | Then commit this new file with the specified commit_message. |
117 | """ |
118 | - # Default to simple '\n' line endings. |
119 | - content = normalize_line_endings(content) |
120 | - # Make sure the content ends with a new-line. This makes |
121 | - # end of file conflicts nicer. |
122 | - if not content.endswith('\n'): |
123 | - content += '\n' |
124 | - t = self.working_tree.bzrdir.root_transport |
125 | + content = normalize_content(content) |
126 | + t = self.tree.bzrdir.root_transport |
127 | # Get a transport for the path we want. |
128 | self._ensure_directory_or_nonexistant(dirname(path)) |
129 | t = t.clone(dirname(path)) |
130 | @@ -120,11 +158,37 @@ |
131 | # Put the file there. |
132 | # TODO: UTF-8 encode text files? |
133 | t.put_bytes(basename(path), content) |
134 | - self.working_tree.smart_add([t.local_abspath('.')]) |
135 | - self.working_tree.commit( |
136 | + self.tree.smart_add([t.local_abspath('.')]) |
137 | + self.tree.commit( |
138 | message=commit_message, |
139 | authors=[author]) |
140 | |
141 | + def _get_final_text(self, content, f, parent_revision): |
142 | + current_rev = f.last_modified_in_revision |
143 | + wt = self.tree |
144 | + current_lines = wt.get_file_lines(f.file_id) |
145 | + basis = self.branch.repository.revision_tree(parent_revision) |
146 | + basis_lines = basis.get_file_lines(f.file_id) |
147 | + # need to break content into lines. |
148 | + ending = get_line_ending(current_lines) |
149 | + # If the content doesn't end with a new line, add one. |
150 | + new_lines = split_lines(content) |
151 | + # Look at the end of the first string. |
152 | + new_ending = get_line_ending(new_lines) |
153 | + if ending != new_ending: |
154 | + # I know this is horribly inefficient, but lets get it working |
155 | + # first. |
156 | + content = normalize_line_endings(content, ending) |
157 | + new_lines = split_lines(content) |
158 | + if len(new_lines) > 0 and not new_lines[-1].endswith(ending): |
159 | + new_lines[-1] += ending |
160 | + merge = Merge3(basis_lines, new_lines, current_lines) |
161 | + result = list(merge.merge_lines()) # or merge_regions or whatever |
162 | + conflicted = ('>>>>>>>' + ending) in result |
163 | + if conflicted: |
164 | + raise UpdateConflicts(''.join(result), current_rev) |
165 | + return result |
166 | + |
167 | def _update_file(self, file_id, path, content, author, parent_revision, |
168 | commit_message): |
169 | """Update an existing file with the content. |
170 | @@ -132,36 +196,14 @@ |
171 | This method merges the changes in based on the parent revision. |
172 | """ |
173 | f = File(self, path, file_id) |
174 | - current_rev = f.last_modified_in_revision |
175 | - wt = self.working_tree |
176 | + wt = self.tree |
177 | wt.lock_write() |
178 | try: |
179 | - current_lines = wt.get_file_lines(file_id) |
180 | - basis = wt.branch.repository.revision_tree(parent_revision) |
181 | - basis_lines = basis.get_file_lines(file_id) |
182 | - # need to break content into lines. |
183 | - ending = get_line_ending(current_lines) |
184 | - # If the content doesn't end with a new line, add one. |
185 | - new_lines = split_lines(content) |
186 | - # Look at the end of the first string. |
187 | - new_ending = get_line_ending(new_lines) |
188 | - if ending != new_ending: |
189 | - # I know this is horribly inefficient, but lets get it working |
190 | - # first. |
191 | - content = normalize_line_endings(content, ending) |
192 | - new_lines = split_lines(content) |
193 | - if len(new_lines) > 0 and not new_lines[-1].endswith(ending): |
194 | - new_lines[-1] += ending |
195 | - merge = Merge3(basis_lines, new_lines, current_lines) |
196 | - result = list(merge.merge_lines()) # or merge_regions or whatever |
197 | - conflicted = ('>>>>>>>' + ending) in result |
198 | - if conflicted: |
199 | - raise UpdateConflicts(''.join(result), current_rev) |
200 | - else: |
201 | - wt.bzrdir.root_transport.put_bytes(path, ''.join(result)) |
202 | - wt.commit( |
203 | - message=commit_message, authors=[author], |
204 | - specific_files=[path]) |
205 | + result = self._get_final_text(content, f, parent_revision) |
206 | + wt.bzrdir.root_transport.put_bytes(path, ''.join(result)) |
207 | + wt.commit( |
208 | + message=commit_message, authors=[author], |
209 | + specific_files=[path]) |
210 | finally: |
211 | wt.unlock() |
212 | |
213 | @@ -177,7 +219,7 @@ |
214 | if directory is None or directory.file_type != FileType.DIRECTORY: |
215 | return None |
216 | listing = [] |
217 | - wt = self.working_tree |
218 | + wt = self.tree |
219 | wt.lock_read() |
220 | try: |
221 | for fp, fc, fkind, fid, entry in wt.list_files( |
222 | @@ -204,9 +246,9 @@ |
223 | BaseFile.__init__(self, path, file_id) |
224 | self.filestore = filestore |
225 | # This isn't entirely necessary. |
226 | - self.working_tree = self.filestore.working_tree |
227 | + self.tree = self.filestore.tree |
228 | self.file_type = self._get_filetype() |
229 | - bt = self.working_tree.basis_tree() |
230 | + bt = self.filestore.basis_tree() |
231 | bt.lock_read() |
232 | try: |
233 | inv_file = bt.inventory[self.file_id] |
234 | @@ -216,7 +258,7 @@ |
235 | |
236 | def _get_filetype(self): |
237 | """Work out the filetype based on the mimetype if possible.""" |
238 | - is_directory = ('directory' == self.working_tree.kind(self.file_id)) |
239 | + is_directory = ('directory' == self.tree.kind(self.file_id)) |
240 | if is_directory: |
241 | return FileType.DIRECTORY |
242 | else: |
243 | @@ -232,27 +274,27 @@ |
244 | def get_content(self): |
245 | if self.file_id is None: |
246 | return None |
247 | - self.working_tree.lock_read() |
248 | + self.tree.lock_read() |
249 | try: |
250 | # basis_tree is a revision tree, queries the repositry. |
251 | # to get the stuff off the filesystem use the working tree |
252 | # which needs to start with that. WorkingTree.open('.'). |
253 | # branch = tree.branch. |
254 | - return self.working_tree.get_file_text(self.file_id) |
255 | + return self.tree.get_file_text(self.file_id) |
256 | finally: |
257 | - self.working_tree.unlock() |
258 | + self.tree.unlock() |
259 | |
260 | @property |
261 | def last_modified_by(self): |
262 | """Return the first author for the revision.""" |
263 | - repo = self.working_tree.branch.repository |
264 | + repo = self.filestore.branch.repository |
265 | rev = repo.get_revision(self.last_modified_in_revision) |
266 | return rev.get_apparent_authors()[0] |
267 | |
268 | @property |
269 | def last_modified_date(self): |
270 | """Return the last modified date for the revision.""" |
271 | - repo = self.working_tree.branch.repository |
272 | + repo = self.filestore.branch.repository |
273 | rev = repo.get_revision(self.last_modified_in_revision) |
274 | return datetime.utcfromtimestamp(rev.timestamp) |
275 | |
276 | @@ -260,7 +302,12 @@ |
277 | def _is_binary(self): |
278 | """True if the file is binary.""" |
279 | try: |
280 | - check_text_path(self.working_tree.abspath(self.path)) |
281 | + self.tree.lock_read() |
282 | + try: |
283 | + lines = self.tree.get_file_lines(self.file_id) |
284 | + check_text_lines(lines) |
285 | + finally: |
286 | + self.tree.unlock() |
287 | return False |
288 | except BinaryFile: |
289 | return True |
290 | @@ -268,7 +315,53 @@ |
291 | @property |
292 | def is_directory(self): |
293 | """Is this file a directory?""" |
294 | - return 'directory' == self.working_tree.kind(self.file_id) |
295 | + return 'directory' == self.tree.kind(self.file_id) |
296 | |
297 | def update(self, content, user): |
298 | raise NotImplementedError() |
299 | + |
300 | + |
301 | +class BranchFileStore(FileStore): |
302 | + |
303 | + def __init__(self, branch): |
304 | + self.branch = branch |
305 | + self.tree = branch.basis_tree() |
306 | + self.logger = logging.getLogger('wikkid') |
307 | + |
308 | + def basis_tree(self): |
309 | + return self.tree |
310 | + |
311 | + def update_file(self, path, content, author, parent_revision, |
312 | + commit_message=None): |
313 | + commit_message = get_commit_message(commit_message) |
314 | + self.branch.lock_write() |
315 | + try: |
316 | + file_id = self.tree.path2id(path) |
317 | + if file_id is not None: |
318 | + f = File(self, path, file_id) |
319 | + content = self._get_final_text(content, f, parent_revision) |
320 | + else: |
321 | + content = normalize_content(content) |
322 | + |
323 | + with TransformPreview(self.tree) as tt: |
324 | + trans_id = tt.trans_id_tree_path(path) |
325 | + if tt.tree_kind(trans_id) is not None: |
326 | + tt.delete_contents(trans_id) |
327 | + else: |
328 | + tt.version_file(trans_id, trans_id) |
329 | + create_parents(tt, path, trans_id) |
330 | + tt.create_file(content, trans_id) |
331 | + try: |
332 | + tt.commit(self.branch, commit_message, authors=[author]) |
333 | + except MalformedTransform, e: |
334 | + for conflict in e.conflicts: |
335 | + if conflict[0] == 'non-directory parent': |
336 | + path = FinalPaths(tt).get_path(trans_id) |
337 | + raise FileExists( |
338 | + '%s exists and is not a directory' % |
339 | + conflict[1]) |
340 | + raise |
341 | + |
342 | + self.tree = self.branch.basis_tree() |
343 | + finally: |
344 | + self.branch.unlock() |
345 | |
346 | === modified file 'wikkid/tests/test_bzr_filestore.py' |
347 | --- wikkid/tests/test_bzr_filestore.py 2010-10-17 21:04:36 +0000 |
348 | +++ wikkid/tests/test_bzr_filestore.py 2011-06-30 16:12:03 +0000 |
349 | @@ -11,7 +11,10 @@ |
350 | from bzrlib.tests import TestCaseWithTransport |
351 | |
352 | from wikkid.errors import UpdateConflicts |
353 | -from wikkid.filestore.bzr import FileStore |
354 | +from wikkid.filestore.bzr import ( |
355 | + BranchFileStore, |
356 | + FileStore, |
357 | + ) |
358 | from wikkid.tests import ProvidesMixin |
359 | from wikkid.tests.filestore import TestFileStore |
360 | |
361 | @@ -134,3 +137,14 @@ |
362 | 'test.txt', '', 'Test Author <test@example.com>', base_rev) |
363 | curr = filestore.get_file('test.txt') |
364 | self.assertEqual('', curr.get_content()) |
365 | + |
366 | + |
367 | +class TestBranchFileStore(TestBzrFileStore): |
368 | + |
369 | + def make_filestore(self, contents=None): |
370 | + tree = self.make_branch_and_tree('.') |
371 | + if contents: |
372 | + self.build_tree_contents(contents) |
373 | + tree.smart_add(['.']) |
374 | + tree.commit(message='Initial commit', authors=['test@example.com']) |
375 | + return BranchFileStore(tree.branch) |
Awesome, merging.