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

Proposed by Aaron Bentley on 2011-06-30
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 2011-06-30 Approve on 2011-06-30
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.
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
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)

Subscribers

People subscribed via source and target branches