cc914c7ff8b837a07fa2038da5d0df2468d38fb9
[jelmer/subvertpy.git] / workingtree.py
1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Checkouts and working trees (working copies)."""
17
18 import bzrlib, bzrlib.add
19 from bzrlib import osutils, urlutils
20 from bzrlib.branch import PullResult
21 from bzrlib.bzrdir import BzrDirFormat, BzrDir
22 from bzrlib.errors import (InvalidRevisionId, NotBranchError, NoSuchFile,
23                            NoRepositoryPresent, 
24                            OutOfDateTree, NoWorkingTree, UnsupportedFormatError)
25 from bzrlib.inventory import Inventory, InventoryFile, InventoryLink
26 from bzrlib.lockable_files import LockableFiles
27 from bzrlib.lockdir import LockDir
28 from bzrlib.revision import NULL_REVISION
29 from bzrlib.trace import mutter
30 from bzrlib.transport import get_transport
31 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
32
33 from bzrlib.plugins.svn import core, properties
34 from bzrlib.plugins.svn.auth import create_auth_baton
35 from bzrlib.plugins.svn.branch import SvnBranch
36 from bzrlib.plugins.svn.commit import _revision_id_to_svk_feature
37 from bzrlib.plugins.svn.core import SubversionException
38 from bzrlib.plugins.svn.errors import ERR_FS_TXN_OUT_OF_DATE, ERR_ENTRY_EXISTS, ERR_WC_PATH_NOT_FOUND, ERR_WC_NOT_DIRECTORY, NotSvnBranchPath
39 from bzrlib.plugins.svn.format import get_rich_root_format
40 from bzrlib.plugins.svn.mapping import escape_svn_path
41 from bzrlib.plugins.svn.remote import SvnRemoteAccess
42 from bzrlib.plugins.svn.repository import SvnRepository
43 from bzrlib.plugins.svn.svk import SVN_PROP_SVK_MERGE, parse_svk_features, serialize_svk_features
44 from bzrlib.plugins.svn.transport import (SvnRaTransport, bzr_to_svn_url, 
45                        svn_config) 
46 from bzrlib.plugins.svn.tree import SvnBasisTree
47 from bzrlib.plugins.svn.wc import *
48
49 import os
50 import urllib
51
52 def update_wc(adm, basedir, conn, revnum):
53     # FIXME: honor SVN_CONFIG_SECTION_HELPERS:SVN_CONFIG_OPTION_DIFF3_CMD
54     # FIXME: honor SVN_CONFIG_SECTION_MISCELLANY:SVN_CONFIG_OPTION_USE_COMMIT_TIMES
55     # FIXME: honor SVN_CONFIG_SECTION_MISCELLANY:SVN_CONFIG_OPTION_PRESERVED_CF_EXTS
56     editor = adm.get_update_editor(basedir, False, True)
57     assert editor is not None
58     reporter = conn.do_update(revnum, "", True, editor)
59     adm.crawl_revisions(basedir, reporter, restore_files=False, 
60                         recurse=True, use_commit_times=False)
61     # FIXME: handle externals
62
63
64 def generate_ignore_list(ignore_map):
65     """Create a list of ignores, ordered by directory.
66     
67     :param ignore_map: Dictionary with paths as keys, patterns as values.
68     :return: list of ignores
69     """
70     ignores = []
71     keys = ignore_map.keys()
72     for k in sorted(keys):
73         elements = ["."]
74         if k.strip("/") != "":
75             elements.append(k.strip("/"))
76         elements.append(ignore_map[k].strip("/"))
77         ignores.append("/".join(elements))
78     return ignores
79
80
81 class SvnWorkingTree(WorkingTree):
82     """WorkingTree implementation that uses a Subversion Working Copy for storage."""
83     def __init__(self, bzrdir, local_path, branch):
84         version = check_wc(local_path)
85         self._format = SvnWorkingTreeFormat(version)
86         self.basedir = local_path
87         assert isinstance(self.basedir, unicode)
88         self.bzrdir = bzrdir
89         self._branch = branch
90         self.base_revnum = 0
91
92         self._get_wc()
93         max_rev = revision_status(self.basedir, None, True)[1]
94         self.base_revnum = max_rev
95         self.base_revid = branch.generate_revision_id(self.base_revnum)
96         self.base_tree = SvnBasisTree(self)
97
98         self.read_working_inventory()
99
100         self._detect_case_handling()
101         self._transport = bzrdir.get_workingtree_transport(None)
102         self.controldir = os.path.join(bzrdir.svn_controldir, 'bzr')
103         try:
104             os.makedirs(self.controldir)
105             os.makedirs(os.path.join(self.controldir, 'lock'))
106         except OSError:
107             pass
108         control_transport = bzrdir.transport.clone(urlutils.join(
109                                                    get_adm_dir(), 'bzr'))
110         self._control_files = LockableFiles(control_transport, 'lock', LockDir)
111
112     def get_ignore_list(self):
113         ignores = set([get_adm_dir()])
114         ignores.update(svn_config.get_default_ignores())
115
116         def dir_add(wc, prefix, patprefix):
117             ignorestr = wc.prop_get(properties.PROP_IGNORE, 
118                                     self.abspath(prefix).rstrip("/"))
119             if ignorestr is not None:
120                 for pat in ignorestr.splitlines():
121                     ignores.add(urlutils.joinpath(patprefix, pat))
122
123             entries = wc.entries_read(False)
124             for entry in entries:
125                 if entry == "":
126                     continue
127
128                 # Ignore ignores on things that aren't directories
129                 if entries[entry].kind != core.NODE_DIR:
130                     continue
131
132                 subprefix = os.path.join(prefix, entry)
133
134                 subwc = WorkingCopy(wc, self.abspath(subprefix))
135                 try:
136                     dir_add(subwc, subprefix, urlutils.joinpath(patprefix, entry))
137                 finally:
138                     subwc.close()
139
140         wc = self._get_wc()
141         try:
142             dir_add(wc, "", ".")
143         finally:
144             wc.close()
145
146         return ignores
147
148     def is_control_filename(self, path):
149         return is_adm_dir(path)
150
151     def apply_inventory_delta(self, changes):
152         raise NotImplementedError(self.apply_inventory_delta)
153
154     def _update(self, revnum=None):
155         if revnum is None:
156             # FIXME: should be able to use -1 here
157             revnum = self.branch.get_revnum()
158         adm = self._get_wc(write_lock=True)
159         try:
160             conn = self.branch.repository.transport.get_connection(self.branch.get_branch_path())
161             try:
162                 update_wc(adm, self.basedir, conn, revnum)
163             finally:
164                 self.branch.repository.transport.add_connection(conn)
165         finally:
166             adm.close()
167         return revnum
168
169     def update(self, change_reporter=None, possible_transports=None, revnum=None):
170         orig_revnum = self.base_revnum
171         self.base_revnum = self._update(revnum)
172         self.base_revid = self.branch.generate_revision_id(self.base_revnum)
173         self.base_tree = None
174         self.read_working_inventory()
175         return self.base_revnum - orig_revnum
176
177     def remove(self, files, verbose=False, to_file=None, keep_files=True, 
178                force=False):
179         # FIXME: Use to_file argument
180         # FIXME: Use verbose argument
181         assert isinstance(files, list)
182         wc = self._get_wc(write_lock=True)
183         try:
184             for file in files:
185                 wc.delete(self.abspath(file))
186         finally:
187             wc.close()
188
189         for file in files:
190             self._change_fileid_mapping(None, file)
191         self.read_working_inventory()
192
193     def _get_wc(self, relpath="", write_lock=False):
194         return WorkingCopy(None, self.abspath(relpath).rstrip("/"), 
195                                 write_lock)
196
197     def _get_rel_wc(self, relpath, write_lock=False):
198         dir = os.path.dirname(relpath)
199         file = os.path.basename(relpath)
200         return (self._get_wc(dir, write_lock), file)
201
202     def move(self, from_paths, to_dir=None, after=False, **kwargs):
203         # FIXME: Use after argument
204         assert after != True
205         for entry in from_paths:
206             try:
207                 to_wc = self._get_wc(to_dir, write_lock=True)
208                 to_wc.copy(self.abspath(entry), os.path.basename(entry))
209             finally:
210                 to_wc.close()
211             try:
212                 from_wc = self._get_wc(write_lock=True)
213                 from_wc.delete(self.abspath(entry))
214             finally:
215                 from_wc.close()
216             new_name = urlutils.join(to_dir, os.path.basename(entry))
217             self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
218             self._change_fileid_mapping(None, entry)
219
220         self.read_working_inventory()
221
222     def rename_one(self, from_rel, to_rel, after=False):
223         # FIXME: Use after
224         assert after != True
225         (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
226         if os.path.dirname(from_rel) == os.path.dirname(to_rel):
227             # Prevent lock contention
228             from_wc = to_wc
229         else:
230             (from_wc, _) = self._get_rel_wc(from_rel, write_lock=True)
231         from_id = self.inventory.path2id(from_rel)
232         try:
233             to_wc.copy(self.abspath(from_rel), to_file)
234             from_wc.delete(self.abspath(from_rel))
235         finally:
236             to_wc.close()
237         self._change_fileid_mapping(None, from_rel)
238         self._change_fileid_mapping(from_id, to_rel)
239         self.read_working_inventory()
240
241     def path_to_file_id(self, revnum, current_revnum, path):
242         """Generate a bzr file id from a Subversion file name. 
243         
244         :param revnum: Revision number.
245         :param path: Absolute path within the Subversion repository.
246         :return: Tuple with file id and revision id.
247         """
248         assert isinstance(revnum, int) and revnum >= 0
249         assert isinstance(path, str)
250
251         rp = self.branch.unprefix(path)
252         entry = self.basis_tree().id_map[rp.decode("utf-8")]
253         assert entry[0] is not None
254         assert isinstance(entry[0], str), "fileid %r for %r is not a string" % (entry[0], path)
255         return entry
256
257     def read_working_inventory(self):
258         inv = Inventory()
259
260         def add_file_to_inv(relpath, id, revid, parent_id):
261             """Add a file to the inventory."""
262             assert isinstance(relpath, unicode)
263             if os.path.islink(self.abspath(relpath)):
264                 file = InventoryLink(id, os.path.basename(relpath), parent_id)
265                 file.revision = revid
266                 file.symlink_target = os.readlink(self.abspath(relpath))
267                 file.text_sha1 = None
268                 file.text_size = None
269                 file.executable = False
270                 inv.add(file)
271             else:
272                 file = InventoryFile(id, os.path.basename(relpath), parent_id)
273                 file.revision = revid
274                 try:
275                     data = osutils.fingerprint_file(open(self.abspath(relpath)))
276                     file.text_sha1 = data['sha1']
277                     file.text_size = data['size']
278                     file.executable = self.is_executable(id, relpath)
279                     inv.add(file)
280                 except IOError:
281                     # Ignore non-existing files
282                     pass
283
284         def find_copies(url, relpath=""):
285             wc = self._get_wc(relpath)
286             entries = wc.entries_read(False)
287             for entry in entries.values():
288                 subrelpath = os.path.join(relpath, entry.name)
289                 if entry.name == "" or entry.kind != 'directory':
290                     if ((entry.copyfrom_url == url or entry.url == url) and 
291                         not (entry.schedule in (SCHEDULE_DELETE,
292                                                 SCHEDULE_REPLACE))):
293                         yield os.path.join(
294                                 self.branch.get_branch_path().strip("/"), 
295                                 subrelpath)
296                 else:
297                     find_copies(subrelpath)
298             wc.close()
299
300         def find_ids(entry, rootwc):
301             relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
302             assert entry.schedule in (SCHEDULE_NORMAL, 
303                                       SCHEDULE_DELETE,
304                                       SCHEDULE_ADD,
305                                       SCHEDULE_REPLACE)
306             if entry.schedule == SCHEDULE_NORMAL:
307                 assert entry.revision >= 0
308                 # Keep old id
309                 return self.path_to_file_id(entry.cmt_rev, entry.revision, 
310                         relpath)
311             elif entry.schedule == SCHEDULE_DELETE:
312                 return (None, None)
313             elif (entry.schedule == SCHEDULE_ADD or 
314                   entry.schedule == SCHEDULE_REPLACE):
315                 # See if the file this file was copied from disappeared
316                 # and has no other copies -> in that case, take id of other file
317                 if (entry.copyfrom_url and 
318                     list(find_copies(entry.copyfrom_url)) == [relpath]):
319                     return self.path_to_file_id(entry.copyfrom_rev, 
320                         entry.revision, entry.copyfrom_url[len(entry.repos):])
321                 ids = self._get_new_file_ids(rootwc)
322                 if ids.has_key(relpath):
323                     return (ids[relpath], None)
324                 # FIXME: Generate more random file ids
325                 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
326
327         def add_dir_to_inv(relpath, wc, parent_id):
328             assert isinstance(relpath, unicode)
329             entries = wc.entries_read(False)
330             entry = entries[""]
331             assert parent_id is None or isinstance(parent_id, str), \
332                     "%r is not a string" % parent_id
333             (id, revid) = find_ids(entry, rootwc)
334             if id is None:
335                 mutter('no id for %r', entry.url)
336                 return
337             assert revid is None or isinstance(revid, str), "%r is not a string" % revid
338             assert isinstance(id, str), "%r is not a string" % id
339
340             # First handle directory itself
341             inv.add_path(relpath.decode("utf-8"), 'directory', id, parent_id).revision = revid
342             if relpath == "":
343                 inv.revision_id = revid
344
345             for name in entries:
346                 if name == "":
347                     continue
348
349                 subrelpath = os.path.join(relpath, name.decode("utf-8"))
350
351                 entry = entries[name]
352                 assert entry
353                 
354                 if entry.kind == core.NODE_DIR:
355                     subwc = WorkingCopy(wc, self.abspath(subrelpath))
356                     try:
357                         assert isinstance(subrelpath, unicode)
358                         add_dir_to_inv(subrelpath, subwc, id)
359                     finally:
360                         subwc.close()
361                 else:
362                     (subid, subrevid) = find_ids(entry, rootwc)
363                     if subid:
364                         assert isinstance(subrelpath, unicode)
365                         add_file_to_inv(subrelpath, subid, subrevid, id)
366                     else:
367                         mutter('no id for %r', entry.url)
368
369         rootwc = self._get_wc() 
370         try:
371             add_dir_to_inv(u"", rootwc, None)
372         finally:
373             rootwc.close()
374
375         self._set_inventory(inv, dirty=False)
376         return inv
377
378     def set_last_revision(self, revid):
379         mutter('setting last revision to %r', revid)
380         if revid is None or revid == NULL_REVISION:
381             self.base_revid = revid
382             self.base_revnum = 0
383             self.base_tree = None
384             return
385
386         rev = self.branch.lookup_revision_id(revid)
387         self.base_revnum = rev
388         self.base_revid = revid
389         self.base_tree = None
390
391     def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
392         """See MutableTree.set_parent_trees."""
393         self.set_parent_ids([rev for (rev, tree) in parents_list])
394
395     def set_parent_ids(self, parent_ids):
396         super(SvnWorkingTree, self).set_parent_ids(parent_ids)
397         if parent_ids == [] or parent_ids[0] == NULL_REVISION:
398             merges = []
399         else:
400             merges = parent_ids[1:]
401         adm = self._get_wc(write_lock=True)
402         try:
403             svk_merges = parse_svk_features(self._get_svk_merges(self._get_base_branch_props()))
404
405             # Set svk:merge
406             for merge in merges:
407                 try:
408                     svk_merges.add(_revision_id_to_svk_feature(merge))
409                 except InvalidRevisionId:
410                     pass
411
412             adm.prop_set(SVN_PROP_SVK_MERGE, 
413                          serialize_svk_features(svk_merges), self.basedir)
414         finally:
415             adm.close()
416
417     def smart_add(self, file_list, recurse=True, action=None, save=True):
418         assert isinstance(recurse, bool)
419         if action is None:
420             action = bzrlib.add.AddAction()
421         # TODO: use action
422         if not file_list:
423             # no paths supplied: add the entire tree.
424             file_list = [u'.']
425         ignored = {}
426         added = []
427
428         for file_path in file_list:
429             todo = []
430             file_path = os.path.abspath(file_path)
431             f = self.relpath(file_path)
432             wc = self._get_wc(os.path.dirname(f), write_lock=True)
433             try:
434                 if not self.inventory.has_filename(f):
435                     if save:
436                         mutter('adding %r', file_path)
437                         wc.add(file_path)
438                     added.append(file_path)
439                 if recurse and osutils.file_kind(file_path) == 'directory':
440                     # Filter out ignored files and update ignored
441                     for c in os.listdir(file_path):
442                         if self.is_control_filename(c):
443                             continue
444                         c_path = os.path.join(file_path, c)
445                         ignore_glob = self.is_ignored(c)
446                         if ignore_glob is not None:
447                             ignored.setdefault(ignore_glob, []).append(c_path)
448                         todo.append(c_path)
449             finally:
450                 wc.close()
451             if todo != []:
452                 cadded, cignored = self.smart_add(todo, recurse, action, save)
453                 added.extend(cadded)
454                 ignored.update(cignored)
455         return added, ignored
456
457     def add(self, files, ids=None, kinds=None):
458         # TODO: Use kinds
459         if isinstance(files, str):
460             files = [files]
461             if isinstance(ids, str):
462                 ids = [ids]
463         if ids is not None:
464             ids = iter(ids)
465         assert isinstance(files, list)
466         for f in files:
467             wc = self._get_wc(os.path.dirname(f), write_lock=True)
468             try:
469                 try:
470                     wc.add(self.abspath(f))
471                     if ids is not None:
472                         self._change_fileid_mapping(ids.next(), f, wc)
473                 except SubversionException, (_, num):
474                     if num == ERR_ENTRY_EXISTS:
475                         continue
476                     elif num == ERR_WC_PATH_NOT_FOUND:
477                         raise NoSuchFile(path=f)
478                     raise
479             finally:
480                 wc.close()
481         self.read_working_inventory()
482
483     def basis_tree(self):
484         if self.base_revid is None or self.base_revid == NULL_REVISION:
485             return self.branch.repository.revision_tree(self.base_revid)
486
487         if self.base_tree is None:
488             self.base_tree = SvnBasisTree(self)
489
490         return self.base_tree
491
492     def pull(self, source, overwrite=False, stop_revision=None, 
493              delta_reporter=None, possible_transports=None):
494         # FIXME: Use delta_reporter
495         # FIXME: Use source
496         # FIXME: Use overwrite
497         result = self.branch.pull(source, overwrite=overwrite, stop_revision=stop_revision)
498         fetched = self._update(self.branch.get_revnum())
499         self.base_revnum = fetched
500         self.base_revid = self.branch.generate_revision_id(fetched)
501         self.base_tree = None
502         self.read_working_inventory()
503         return result
504
505     def get_file_sha1(self, file_id, path=None, stat_value=None):
506         if not path:
507             path = self._inventory.id2path(file_id)
508         return osutils.fingerprint_file(open(self.abspath(path)))['sha1']
509
510     def _change_fileid_mapping(self, id, path, wc=None):
511         if wc is None:
512             subwc = self._get_wc(write_lock=True)
513         else:
514             subwc = wc
515         new_entries = self._get_new_file_ids(subwc)
516         if id is None:
517             if new_entries.has_key(path):
518                 del new_entries[path]
519         else:
520             assert isinstance(id, str)
521             new_entries[path] = id
522         fileprops = self._get_branch_props()
523         self.branch.mapping.export_fileid_map(new_entries, None, fileprops)
524         self._set_branch_props(subwc, fileprops)
525         if wc is None:
526             subwc.close()
527
528     def _get_branch_props(self):
529         wc = self._get_wc()
530         try:
531             (prop_changes, orig_props) = wc.get_prop_diffs(self.basedir)
532             for k,v in prop_changes:
533                 if v is None:
534                     del orig_props[k]
535                 else:
536                     orig_props[k] = v
537             return orig_props
538         finally:
539             wc.close()
540
541     def _set_branch_props(self, wc, fileprops):
542         for k,v in fileprops.items():
543             wc.prop_set(k, v, self.basedir)
544
545     def _get_base_branch_props(self):
546         wc = self._get_wc()
547         try:
548             (prop_changes, orig_props) = wc.get_prop_diffs(self.basedir)
549             return orig_props
550         finally:
551             wc.close()
552
553     def _get_new_file_ids(self, wc):
554         return self.branch.mapping.import_fileid_map({}, 
555                 self._get_branch_props())
556
557     def _get_svk_merges(self, base_branch_props):
558         return base_branch_props.get(SVN_PROP_SVK_MERGE, "")
559
560     def apply_inventory_delta(self, delta):
561         assert delta == []
562
563     def _last_revision(self):
564         return self.base_revid
565
566     def path_content_summary(self, path, _lstat=os.lstat,
567         _mapper=osutils.file_kind_from_stat_mode):
568         """See Tree.path_content_summary."""
569         abspath = self.abspath(path)
570         try:
571             stat_result = _lstat(abspath)
572         except OSError, e:
573             if getattr(e, 'errno', None) == errno.ENOENT:
574                 # no file.
575                 return ('missing', None, None, None)
576             # propagate other errors
577             raise
578         kind = _mapper(stat_result.st_mode)
579         if kind == 'file':
580             size = stat_result.st_size
581             # try for a stat cache lookup
582             executable = self._is_executable_from_path_and_stat(path, stat_result)
583             return (kind, size, executable, self._sha_from_stat(
584                 path, stat_result))
585         elif kind == 'directory':
586             return kind, None, None, None
587         elif kind == 'symlink':
588             return ('symlink', None, None, os.readlink(abspath))
589         else:
590             return (kind, None, None, None)
591
592     def _get_base_revmeta(self):
593         return self.branch.repository._revmeta_provider.get_revision(self.branch.get_branch_path(self.base_revnum), self.base_revnum)
594
595     def _reset_data(self):
596         pass
597
598     def unlock(self):
599         # non-implementation specific cleanup
600         self._cleanup()
601
602         # reverse order of locking.
603         try:
604             return self._control_files.unlock()
605         finally:
606             self.branch.unlock()
607
608     if not osutils.supports_executable():
609         def is_executable(self, file_id, path=None):
610             inv = self.basis_tree()._inventory
611             if file_id in inv:
612                 return inv[file_id].executable
613             # Default to not executable
614             return False
615
616     def update_basis_by_delta(self, new_revid, delta):
617         """Update the parents of this tree after a commit.
618
619         This gives the tree one parent, with revision id new_revid. The
620         inventory delta is applied to the current basis tree to generate the
621         inventory for the parent new_revid, and all other parent trees are
622         discarded.
623
624         All the changes in the delta should be changes synchronising the basis
625         tree with some or all of the working tree, with a change to a directory
626         requiring that its contents have been recursively included. That is,
627         this is not a general purpose tree modification routine, but a helper
628         for commit which is not required to handle situations that do not arise
629         outside of commit.
630
631         :param new_revid: The new revision id for the trees parent.
632         :param delta: An inventory delta (see apply_inventory_delta) describing
633             the changes from the current left most parent revision to new_revid.
634         """
635         rev = self.branch.lookup_revision_id(new_revid)
636         self.base_revnum = rev
637         self.base_revid = new_revid
638         self.base_tree = None
639
640         # TODO: Implement more efficient version
641         newrev = self.branch.repository.get_revision(new_revid)
642         newrevtree = self.branch.repository.revision_tree(new_revid)
643         svn_revprops = self.branch.repository._log.revprop_list(rev)
644
645         def update_settings(wc, path):
646             id = newrevtree.inventory.path2id(path)
647             mutter("Updating settings for %r", id)
648             revnum = self.branch.lookup_revision_id(
649                     newrevtree.inventory[id].revision)
650
651             if newrevtree.inventory[id].kind != 'directory':
652                 return
653
654             entries = wc.entries_read(True)
655             for name, entry in entries.items():
656                 if name == "":
657                     continue
658
659                 wc.process_committed(self.abspath(path).rstrip("/"), 
660                               False, self.branch.lookup_revision_id(newrevtree.inventory[id].revision),
661                               svn_revprops[properties.PROP_REVISION_DATE], 
662                               svn_revprops.get(properties.PROP_REVISION_AUTHOR, ""))
663
664                 child_path = os.path.join(path, name.decode("utf-8"))
665
666                 fileid = newrevtree.inventory.path2id(child_path)
667
668                 if newrevtree.inventory[fileid].kind == 'directory':
669                     subwc = WorkingCopy(wc, self.abspath(child_path).rstrip("/"), write_lock=True)
670                     try:
671                         update_settings(subwc, child_path)
672                     finally:
673                         subwc.close()
674
675         # Set proper version for all files in the wc
676         wc = self._get_wc(write_lock=True)
677         try:
678             wc.process_committed(self.basedir,
679                           False, self.branch.lookup_revision_id(newrevtree.inventory.root.revision),
680                           svn_revprops[properties.PROP_REVISION_DATE], 
681                           svn_revprops.get(properties.PROP_REVISION_AUTHOR, ""))
682             update_settings(wc, "")
683         finally:
684             wc.close()
685
686         self.set_parent_ids([new_revid])
687
688
689 class SvnWorkingTreeFormat(WorkingTreeFormat):
690     """Subversion working copy format."""
691     def __init__(self, version):
692         self.version = version
693
694     def __get_matchingbzrdir(self):
695         return SvnWorkingTreeDirFormat()
696
697     _matchingbzrdir = property(__get_matchingbzrdir)
698
699     def get_format_description(self):
700         return "Subversion Working Copy Version %d" % self.version
701
702     def get_format_string(self):
703         raise NotImplementedError
704
705     def initialize(self, a_bzrdir, revision_id=None):
706         raise NotImplementedError(self.initialize)
707
708     def open(self, a_bzrdir):
709         raise NotImplementedError(self.initialize)
710
711
712 class SvnCheckout(BzrDir):
713     """BzrDir implementation for Subversion checkouts (directories 
714     containing a .svn subdirectory."""
715     def __init__(self, transport, format):
716         super(SvnCheckout, self).__init__(transport, format)
717         self.local_path = transport.local_abspath(".")
718         
719         # Open related remote repository + branch
720         try:
721             wc = WorkingCopy(None, self.local_path)
722         except SubversionException, (msg, ERR_WC_UNSUPPORTED_FORMAT):
723             raise UnsupportedFormatError(msg, kind='workingtree')
724         try:
725             self.svn_url = wc.entry(self.local_path, True).url
726         finally:
727             wc.close()
728
729         self._remote_transport = None
730         self._remote_bzrdir = None
731         self.svn_controldir = os.path.join(self.local_path, get_adm_dir())
732         self.root_transport = self.transport = transport
733
734     def get_remote_bzrdir(self):
735         if self._remote_bzrdir is None:
736             self._remote_bzrdir = SvnRemoteAccess(self.get_remote_transport())
737         return self._remote_bzrdir
738
739     def get_remote_transport(self):
740         if self._remote_transport is None:
741             self._remote_transport = SvnRaTransport(self.svn_url)
742         return self._remote_transport
743         
744     def clone(self, path, revision_id=None, force_new_repo=False):
745         raise NotImplementedError(self.clone)
746
747     def open_workingtree(self, _unsupported=False, recommend_upgrade=False):
748         try:
749             return SvnWorkingTree(self, self.local_path, self.open_branch())
750         except NotSvnBranchPath, e:
751             raise NoWorkingTree(self.local_path)
752
753     def sprout(self, url, revision_id=None, force_new_repo=False, 
754                recurse='down', possible_transports=None, accelerator_tree=None,
755                hardlink=False):
756         # FIXME: honor force_new_repo
757         # FIXME: Use recurse
758         result = get_rich_root_format().initialize(url)
759         repo = self._find_repository()
760         repo.clone(result, revision_id)
761         branch = self.open_branch()
762         branch.sprout(result, revision_id)
763         result.create_workingtree(hardlink=hardlink)
764         return result
765
766     def open_repository(self):
767         raise NoRepositoryPresent(self)
768
769     def find_repository(self):
770         raise NoRepositoryPresent(self)
771
772     def _find_repository(self):
773         return SvnRepository(self, self.get_remote_transport().clone_root(), 
774                              self.get_remote_bzrdir().branch_path)
775
776     def needs_format_conversion(self, format=None):
777         if format is None:
778             format = BzrDirFormat.get_default_format()
779         return not isinstance(self._format, format.__class__)
780
781     def get_workingtree_transport(self, format):
782         assert format is None
783         return get_transport(self.svn_controldir)
784
785     def create_workingtree(self, revision_id=None, hardlink=None):
786         """See BzrDir.create_workingtree().
787
788         Not implemented for Subversion because having a .svn directory
789         implies having a working copy.
790         """
791         raise NotImplementedError(self.create_workingtree)
792
793     def create_branch(self):
794         """See BzrDir.create_branch()."""
795         raise NotImplementedError(self.create_branch)
796
797     def open_branch(self, unsupported=True):
798         """See BzrDir.open_branch()."""
799         repos = self._find_repository()
800
801         try:
802             branch = SvnBranch(repos, self.get_remote_bzrdir().branch_path)
803         except SubversionException, (_, num):
804             if num == ERR_WC_NOT_DIRECTORY:
805                 raise NotBranchError(path=self.base)
806             raise
807
808         branch.bzrdir = self.get_remote_bzrdir()
809  
810         return branch