Merge lp:~abentley/wikkid/branch-file-store into lp:wikkid

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
Reviewer Review Type Date Requested Status
Tim Penhey Approve
Review via email: mp+66484@code.launchpad.net

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.
Revision history for this message
Tim Penhey (thumper) wrote :

Awesome, merging.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'wikkid/filestore/bzr.py'
--- wikkid/filestore/bzr.py 2010-10-17 01:47:00 +0000
+++ wikkid/filestore/bzr.py 2011-06-30 16:12:03 +0000
@@ -11,11 +11,12 @@
1111
12from zope.interface import implements12from zope.interface import implements
1313
14from bzrlib.errors import BinaryFile14from bzrlib.errors import BinaryFile, MalformedTransform
15from bzrlib.merge3 import Merge315from bzrlib.merge3 import Merge3
16from bzrlib.osutils import split_lines16from bzrlib.osutils import splitpath, split_lines
17from bzrlib.revision import NULL_REVISION17from bzrlib.revision import NULL_REVISION
18from bzrlib.textfile import check_text_path18from bzrlib.textfile import check_text_lines
19from bzrlib.transform import FinalPaths, TransformPreview
19from bzrlib.urlutils import basename, dirname, joinpath20from bzrlib.urlutils import basename, dirname, joinpath
2021
21from wikkid.errors import FileExists, UpdateConflicts22from wikkid.errors import FileExists, UpdateConflicts
@@ -38,18 +39,61 @@
38 return '\n'39 return '\n'
3940
4041
42def get_commit_message(commit_message):
43 if commit_message is None or commit_message.strip() == '':
44 return 'No description of change given.'
45 return commit_message
46
47
48def normalize_content(content):
49 # Default to simple '\n' line endings.
50 content = normalize_line_endings(content)
51 # Make sure the content ends with a new-line. This makes
52 # end of file conflicts nicer.
53 if not content.endswith('\n'):
54 content += '\n'
55 return content
56
57
58def iter_paths(path):
59 path_segments = splitpath(path)
60 while len(path_segments) > 0:
61 tail = path_segments.pop()
62 if len(path_segments) == 0:
63 yield '', tail
64 else:
65 yield joinpath(*path_segments), tail
66
67
68def create_parents(tt, path, trans_id):
69 prev_trans_id = trans_id
70 for parent_path, tail in iter_paths(path):
71 trans_id = tt.trans_id_tree_path(parent_path)
72 tt.adjust_path(tail, trans_id, prev_trans_id)
73 if tt.tree_kind(trans_id) is not None:
74 break
75 tt.create_directory(trans_id)
76 tt.version_file(trans_id, trans_id)
77 prev_name = tail
78 prev_trans_id = trans_id
79
80
41class FileStore(object):81class FileStore(object):
42 """Wraps a Bazaar branch to be a filestore."""82 """Wraps a Bazaar branch to be a filestore."""
4383
44 implements(IFileStore)84 implements(IFileStore)
4585
46 def __init__(self, working_tree):86 def __init__(self, tree):
47 self.working_tree = working_tree87 self.tree = tree
88 self.branch = tree.branch
48 self.logger = logging.getLogger('wikkid')89 self.logger = logging.getLogger('wikkid')
4990
91 def basis_tree(self):
92 return self.tree.basis_tree()
93
50 def get_file(self, path):94 def get_file(self, path):
51 """Return an object representing the file at specified path."""95 """Return an object representing the file at specified path."""
52 file_id = self.working_tree.path2id(path)96 file_id = self.tree.path2id(path)
53 if file_id is None:97 if file_id is None:
54 return None98 return None
55 else:99 else:
@@ -62,16 +106,15 @@
62 This is going to be really interesting when we need to deal with106 This is going to be really interesting when we need to deal with
63 conflicts.107 conflicts.
64 """108 """
65 if commit_message is None or commit_message.strip() == '':109 commit_message = get_commit_message(commit_message)
66 commit_message = 'No description of change given.'
67 if parent_revision is None:110 if parent_revision is None:
68 parent_revision = NULL_REVISION111 parent_revision = NULL_REVISION
69 # Firstly we want to lock the tree for writing.112 # Firstly we want to lock the tree for writing.
70 self.working_tree.lock_write()113 self.tree.lock_write()
71 try:114 try:
72 # Look to see if the path is there. If it is then we are doing an115 # Look to see if the path is there. If it is then we are doing an
73 # update. If it isn't we are doing an add.116 # update. If it isn't we are doing an add.
74 file_id = self.working_tree.path2id(path)117 file_id = self.tree.path2id(path)
75 if file_id is None:118 if file_id is None:
76 self._add_file(path, content, author, commit_message)119 self._add_file(path, content, author, commit_message)
77 else:120 else:
@@ -80,7 +123,7 @@
80 file_id, path, content, author, parent_revision,123 file_id, path, content, author, parent_revision,
81 commit_message)124 commit_message)
82 finally:125 finally:
83 self.working_tree.unlock()126 self.tree.unlock()
84127
85 def _ensure_directory_or_nonexistant(self, dir_path):128 def _ensure_directory_or_nonexistant(self, dir_path):
86 """Ensure the dir_path defines a directory or doesn't exist.129 """Ensure the dir_path defines a directory or doesn't exist.
@@ -106,13 +149,8 @@
106149
107 Then commit this new file with the specified commit_message.150 Then commit this new file with the specified commit_message.
108 """151 """
109 # Default to simple '\n' line endings.152 content = normalize_content(content)
110 content = normalize_line_endings(content)153 t = self.tree.bzrdir.root_transport
111 # Make sure the content ends with a new-line. This makes
112 # end of file conflicts nicer.
113 if not content.endswith('\n'):
114 content += '\n'
115 t = self.working_tree.bzrdir.root_transport
116 # Get a transport for the path we want.154 # Get a transport for the path we want.
117 self._ensure_directory_or_nonexistant(dirname(path))155 self._ensure_directory_or_nonexistant(dirname(path))
118 t = t.clone(dirname(path))156 t = t.clone(dirname(path))
@@ -120,11 +158,37 @@
120 # Put the file there.158 # Put the file there.
121 # TODO: UTF-8 encode text files?159 # TODO: UTF-8 encode text files?
122 t.put_bytes(basename(path), content)160 t.put_bytes(basename(path), content)
123 self.working_tree.smart_add([t.local_abspath('.')])161 self.tree.smart_add([t.local_abspath('.')])
124 self.working_tree.commit(162 self.tree.commit(
125 message=commit_message,163 message=commit_message,
126 authors=[author])164 authors=[author])
127165
166 def _get_final_text(self, content, f, parent_revision):
167 current_rev = f.last_modified_in_revision
168 wt = self.tree
169 current_lines = wt.get_file_lines(f.file_id)
170 basis = self.branch.repository.revision_tree(parent_revision)
171 basis_lines = basis.get_file_lines(f.file_id)
172 # need to break content into lines.
173 ending = get_line_ending(current_lines)
174 # If the content doesn't end with a new line, add one.
175 new_lines = split_lines(content)
176 # Look at the end of the first string.
177 new_ending = get_line_ending(new_lines)
178 if ending != new_ending:
179 # I know this is horribly inefficient, but lets get it working
180 # first.
181 content = normalize_line_endings(content, ending)
182 new_lines = split_lines(content)
183 if len(new_lines) > 0 and not new_lines[-1].endswith(ending):
184 new_lines[-1] += ending
185 merge = Merge3(basis_lines, new_lines, current_lines)
186 result = list(merge.merge_lines()) # or merge_regions or whatever
187 conflicted = ('>>>>>>>' + ending) in result
188 if conflicted:
189 raise UpdateConflicts(''.join(result), current_rev)
190 return result
191
128 def _update_file(self, file_id, path, content, author, parent_revision,192 def _update_file(self, file_id, path, content, author, parent_revision,
129 commit_message):193 commit_message):
130 """Update an existing file with the content.194 """Update an existing file with the content.
@@ -132,36 +196,14 @@
132 This method merges the changes in based on the parent revision.196 This method merges the changes in based on the parent revision.
133 """197 """
134 f = File(self, path, file_id)198 f = File(self, path, file_id)
135 current_rev = f.last_modified_in_revision199 wt = self.tree
136 wt = self.working_tree
137 wt.lock_write()200 wt.lock_write()
138 try:201 try:
139 current_lines = wt.get_file_lines(file_id)202 result = self._get_final_text(content, f, parent_revision)
140 basis = wt.branch.repository.revision_tree(parent_revision)203 wt.bzrdir.root_transport.put_bytes(path, ''.join(result))
141 basis_lines = basis.get_file_lines(file_id)204 wt.commit(
142 # need to break content into lines.205 message=commit_message, authors=[author],
143 ending = get_line_ending(current_lines)206 specific_files=[path])
144 # If the content doesn't end with a new line, add one.
145 new_lines = split_lines(content)
146 # Look at the end of the first string.
147 new_ending = get_line_ending(new_lines)
148 if ending != new_ending:
149 # I know this is horribly inefficient, but lets get it working
150 # first.
151 content = normalize_line_endings(content, ending)
152 new_lines = split_lines(content)
153 if len(new_lines) > 0 and not new_lines[-1].endswith(ending):
154 new_lines[-1] += ending
155 merge = Merge3(basis_lines, new_lines, current_lines)
156 result = list(merge.merge_lines()) # or merge_regions or whatever
157 conflicted = ('>>>>>>>' + ending) in result
158 if conflicted:
159 raise UpdateConflicts(''.join(result), current_rev)
160 else:
161 wt.bzrdir.root_transport.put_bytes(path, ''.join(result))
162 wt.commit(
163 message=commit_message, authors=[author],
164 specific_files=[path])
165 finally:207 finally:
166 wt.unlock()208 wt.unlock()
167209
@@ -177,7 +219,7 @@
177 if directory is None or directory.file_type != FileType.DIRECTORY:219 if directory is None or directory.file_type != FileType.DIRECTORY:
178 return None220 return None
179 listing = []221 listing = []
180 wt = self.working_tree222 wt = self.tree
181 wt.lock_read()223 wt.lock_read()
182 try:224 try:
183 for fp, fc, fkind, fid, entry in wt.list_files(225 for fp, fc, fkind, fid, entry in wt.list_files(
@@ -204,9 +246,9 @@
204 BaseFile.__init__(self, path, file_id)246 BaseFile.__init__(self, path, file_id)
205 self.filestore = filestore247 self.filestore = filestore
206 # This isn't entirely necessary.248 # This isn't entirely necessary.
207 self.working_tree = self.filestore.working_tree249 self.tree = self.filestore.tree
208 self.file_type = self._get_filetype()250 self.file_type = self._get_filetype()
209 bt = self.working_tree.basis_tree()251 bt = self.filestore.basis_tree()
210 bt.lock_read()252 bt.lock_read()
211 try:253 try:
212 inv_file = bt.inventory[self.file_id]254 inv_file = bt.inventory[self.file_id]
@@ -216,7 +258,7 @@
216258
217 def _get_filetype(self):259 def _get_filetype(self):
218 """Work out the filetype based on the mimetype if possible."""260 """Work out the filetype based on the mimetype if possible."""
219 is_directory = ('directory' == self.working_tree.kind(self.file_id))261 is_directory = ('directory' == self.tree.kind(self.file_id))
220 if is_directory:262 if is_directory:
221 return FileType.DIRECTORY263 return FileType.DIRECTORY
222 else:264 else:
@@ -232,27 +274,27 @@
232 def get_content(self):274 def get_content(self):
233 if self.file_id is None:275 if self.file_id is None:
234 return None276 return None
235 self.working_tree.lock_read()277 self.tree.lock_read()
236 try:278 try:
237 # basis_tree is a revision tree, queries the repositry.279 # basis_tree is a revision tree, queries the repositry.
238 # to get the stuff off the filesystem use the working tree280 # to get the stuff off the filesystem use the working tree
239 # which needs to start with that. WorkingTree.open('.').281 # which needs to start with that. WorkingTree.open('.').
240 # branch = tree.branch.282 # branch = tree.branch.
241 return self.working_tree.get_file_text(self.file_id)283 return self.tree.get_file_text(self.file_id)
242 finally:284 finally:
243 self.working_tree.unlock()285 self.tree.unlock()
244286
245 @property287 @property
246 def last_modified_by(self):288 def last_modified_by(self):
247 """Return the first author for the revision."""289 """Return the first author for the revision."""
248 repo = self.working_tree.branch.repository290 repo = self.filestore.branch.repository
249 rev = repo.get_revision(self.last_modified_in_revision)291 rev = repo.get_revision(self.last_modified_in_revision)
250 return rev.get_apparent_authors()[0]292 return rev.get_apparent_authors()[0]
251293
252 @property294 @property
253 def last_modified_date(self):295 def last_modified_date(self):
254 """Return the last modified date for the revision."""296 """Return the last modified date for the revision."""
255 repo = self.working_tree.branch.repository297 repo = self.filestore.branch.repository
256 rev = repo.get_revision(self.last_modified_in_revision)298 rev = repo.get_revision(self.last_modified_in_revision)
257 return datetime.utcfromtimestamp(rev.timestamp)299 return datetime.utcfromtimestamp(rev.timestamp)
258300
@@ -260,7 +302,12 @@
260 def _is_binary(self):302 def _is_binary(self):
261 """True if the file is binary."""303 """True if the file is binary."""
262 try:304 try:
263 check_text_path(self.working_tree.abspath(self.path))305 self.tree.lock_read()
306 try:
307 lines = self.tree.get_file_lines(self.file_id)
308 check_text_lines(lines)
309 finally:
310 self.tree.unlock()
264 return False311 return False
265 except BinaryFile:312 except BinaryFile:
266 return True313 return True
@@ -268,7 +315,53 @@
268 @property315 @property
269 def is_directory(self):316 def is_directory(self):
270 """Is this file a directory?"""317 """Is this file a directory?"""
271 return 'directory' == self.working_tree.kind(self.file_id)318 return 'directory' == self.tree.kind(self.file_id)
272319
273 def update(self, content, user):320 def update(self, content, user):
274 raise NotImplementedError()321 raise NotImplementedError()
322
323
324class BranchFileStore(FileStore):
325
326 def __init__(self, branch):
327 self.branch = branch
328 self.tree = branch.basis_tree()
329 self.logger = logging.getLogger('wikkid')
330
331 def basis_tree(self):
332 return self.tree
333
334 def update_file(self, path, content, author, parent_revision,
335 commit_message=None):
336 commit_message = get_commit_message(commit_message)
337 self.branch.lock_write()
338 try:
339 file_id = self.tree.path2id(path)
340 if file_id is not None:
341 f = File(self, path, file_id)
342 content = self._get_final_text(content, f, parent_revision)
343 else:
344 content = normalize_content(content)
345
346 with TransformPreview(self.tree) as tt:
347 trans_id = tt.trans_id_tree_path(path)
348 if tt.tree_kind(trans_id) is not None:
349 tt.delete_contents(trans_id)
350 else:
351 tt.version_file(trans_id, trans_id)
352 create_parents(tt, path, trans_id)
353 tt.create_file(content, trans_id)
354 try:
355 tt.commit(self.branch, commit_message, authors=[author])
356 except MalformedTransform, e:
357 for conflict in e.conflicts:
358 if conflict[0] == 'non-directory parent':
359 path = FinalPaths(tt).get_path(trans_id)
360 raise FileExists(
361 '%s exists and is not a directory' %
362 conflict[1])
363 raise
364
365 self.tree = self.branch.basis_tree()
366 finally:
367 self.branch.unlock()
275368
=== modified file 'wikkid/tests/test_bzr_filestore.py'
--- wikkid/tests/test_bzr_filestore.py 2010-10-17 21:04:36 +0000
+++ wikkid/tests/test_bzr_filestore.py 2011-06-30 16:12:03 +0000
@@ -11,7 +11,10 @@
11from bzrlib.tests import TestCaseWithTransport11from bzrlib.tests import TestCaseWithTransport
1212
13from wikkid.errors import UpdateConflicts13from wikkid.errors import UpdateConflicts
14from wikkid.filestore.bzr import FileStore14from wikkid.filestore.bzr import (
15 BranchFileStore,
16 FileStore,
17 )
15from wikkid.tests import ProvidesMixin18from wikkid.tests import ProvidesMixin
16from wikkid.tests.filestore import TestFileStore19from wikkid.tests.filestore import TestFileStore
1720
@@ -134,3 +137,14 @@
134 'test.txt', '', 'Test Author <test@example.com>', base_rev)137 'test.txt', '', 'Test Author <test@example.com>', base_rev)
135 curr = filestore.get_file('test.txt')138 curr = filestore.get_file('test.txt')
136 self.assertEqual('', curr.get_content())139 self.assertEqual('', curr.get_content())
140
141
142class TestBranchFileStore(TestBzrFileStore):
143
144 def make_filestore(self, contents=None):
145 tree = self.make_branch_and_tree('.')
146 if contents:
147 self.build_tree_contents(contents)
148 tree.smart_add(['.'])
149 tree.commit(message='Initial commit', authors=['test@example.com'])
150 return BranchFileStore(tree.branch)

Subscribers

People subscribed via source and target branches