The Python Subversion bindings have trouble dealing with unicode commit messages.
[jelmer/subvertpy.git] / checkout.py
1 # Copyright (C) 2005-2006 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 2 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
17 from binascii import hexlify
18 from bzrlib.bzrdir import BzrDirFormat, BzrDir
19 from bzrlib.errors import (InvalidRevisionId, NotBranchError, NoSuchFile,
20                            NoRepositoryPresent, BzrError)
21 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile,
22                               InventoryLink, ROOT_ID)
23 from bzrlib.lockable_files import TransportLock, LockableFiles
24 from bzrlib.lockdir import LockDir
25 from bzrlib.osutils import rand_bytes, fingerprint_file
26 from bzrlib.progress import DummyProgress
27 from bzrlib.revision import NULL_REVISION
28 from bzrlib.trace import mutter
29 from bzrlib.transport.local import LocalTransport
30 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
31
32 from branch import SvnBranch
33 from repository import (SvnRepository, escape_svn_path, SVN_PROP_BZR_MERGE,
34                         SVN_PROP_SVK_MERGE, SVN_PROP_BZR_FILEIDS, 
35                         revision_id_to_svk_feature) 
36 from scheme import BranchingScheme
37 from transport import (SvnRaTransport, svn_config, bzr_to_svn_url, 
38                        _create_auth_baton) 
39 from tree import SvnBasisTree
40
41 from copy import copy
42 import os
43 import urllib
44
45 import svn.core, svn.wc
46 from svn.core import SubversionException, Pool
47
48 class WorkingTreeInconsistent(BzrError):
49     _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
50
51     def __init__(self, min_revnum, max_revnum):
52         self.min_revnum = min_revnum
53         self.max_revnum = max_revnum
54
55
56 class SvnWorkingTree(WorkingTree):
57     """Implementation of WorkingTree that uses a Subversion 
58     Working Copy for storage."""
59     def __init__(self, bzrdir, local_path, branch):
60         self._format = SvnWorkingTreeFormat()
61         self.basedir = local_path
62         self.bzrdir = bzrdir
63         self._branch = branch
64         self.base_revnum = 0
65         self.pool = Pool()
66         self.client_ctx = svn.client.create_context()
67         self.client_ctx.config = svn_config
68         self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
69         self.client_ctx.auth_baton = _create_auth_baton(self.pool)
70
71         wc = self._get_wc()
72         status = svn.wc.revision_status(self.basedir, None, True, None, None)
73         if status.min_rev != status.max_rev:
74             #raise WorkingTreeInconsistent(status.min_rev, status.max_rev)
75             rev = svn.core.svn_opt_revision_t()
76             rev.kind = svn.core.svn_opt_revision_number
77             rev.value.number = status.max_rev
78             assert status.max_rev == svn.client.update(self.basedir, rev,
79                                      True, self.client_ctx, Pool())
80
81         self.base_revnum = status.max_rev
82         self.base_tree = SvnBasisTree(self)
83         self.base_revid = branch.repository.generate_revision_id(
84                     self.base_revnum, branch.branch_path)
85
86         self.read_working_inventory()
87
88         self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
89         try:
90             os.makedirs(self.controldir)
91             os.makedirs(os.path.join(self.controldir, 'lock'))
92         except OSError:
93             pass
94         control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
95         self._control_files = LockableFiles(control_transport, 'lock', LockDir)
96
97     def lock_write(self):
98         pass
99
100     def lock_read(self):
101         pass
102
103     def unlock(self):
104         pass
105
106     def get_ignore_list(self):
107         ignores = [svn.wc.get_adm_dir()] + svn.wc.get_default_ignores(svn_config)
108
109         def dir_add(wc, prefix):
110             ignorestr = svn.wc.prop_get(svn.core.SVN_PROP_IGNORE, self.abspath(prefix).rstrip("/"), wc)
111             if ignorestr is not None:
112                 for pat in ignorestr.splitlines():
113                     ignores.append("./"+os.path.join(prefix, pat))
114
115             entries = svn.wc.entries_read(wc, False)
116             for entry in entries:
117                 if entry == "":
118                     continue
119
120                 if entries[entry].kind != svn.core.svn_node_dir:
121                     continue
122
123                 subprefix = os.path.join(prefix, entry)
124
125                 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
126                 try:
127                     dir_add(subwc, subprefix)
128                 finally:
129                     svn.wc.adm_close(subwc)
130
131         wc = self._get_wc()
132         try:
133             dir_add(wc, "")
134         finally:
135             svn.wc.adm_close(wc)
136
137         return ignores
138
139     def _write_inventory(self, inv):
140         pass
141
142     def is_control_filename(self, path):
143         return svn.wc.is_adm_dir(path)
144
145     def remove(self, files, verbose=False, to_file=None):
146         assert isinstance(files, list)
147         wc = self._get_wc(write_lock=True)
148         try:
149             for file in files:
150                 svn.wc.delete2(self.abspath(file), wc, None, None, None)
151         finally:
152             svn.wc.adm_close(wc)
153
154         for file in files:
155             self._change_fileid_mapping(None, file)
156         self.read_working_inventory()
157
158     def _get_wc(self, relpath="", write_lock=False):
159         return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), 
160                                 write_lock, 0, None)
161
162     def _get_rel_wc(self, relpath, write_lock=False):
163         dir = os.path.dirname(relpath)
164         file = os.path.basename(relpath)
165         return (self._get_wc(dir, write_lock), file)
166
167     def move(self, from_paths, to_name):
168         revt = svn.core.svn_opt_revision_t()
169         revt.kind = svn.core.svn_opt_revision_working
170         for entry in from_paths:
171             try:
172                 to_wc = self._get_wc(to_name, write_lock=True)
173                 svn.wc.copy(self.abspath(entry), to_wc, 
174                             os.path.basename(entry), None, None)
175             finally:
176                 svn.wc.adm_close(to_wc)
177             try:
178                 from_wc = self._get_wc(write_lock=True)
179                 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
180             finally:
181                 svn.wc.adm_close(from_wc)
182             new_name = "%s/%s" % (to_name, os.path.basename(entry))
183             self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
184             self._change_fileid_mapping(None, entry)
185
186         self.read_working_inventory()
187
188     def rename_one(self, from_rel, to_rel):
189         revt = svn.core.svn_opt_revision_t()
190         revt.kind = svn.core.svn_opt_revision_unspecified
191         (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
192         from_id = self.inventory.path2id(from_rel)
193         try:
194             svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
195             svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
196         finally:
197             svn.wc.adm_close(to_wc)
198         self._change_fileid_mapping(None, from_rel)
199         self._change_fileid_mapping(from_id, to_rel)
200         self.read_working_inventory()
201
202     def path_to_file_id(self, revnum, current_revnum, path):
203         """Generate a bzr file id from a Subversion file name. 
204         
205         :param revnum: Revision number.
206         :param path: Absolute path.
207         :return: Tuple with file id and revision id.
208         """
209         assert isinstance(revnum, int) and revnum >= 0
210         assert isinstance(path, basestring)
211
212         (bp, rp) = self.branch.repository.scheme.unprefix(path)
213         entry = self.base_tree.id_map[rp]
214         assert entry[0] is not None
215         return entry
216
217     def read_working_inventory(self):
218         inv = Inventory()
219
220         def add_file_to_inv(relpath, id, revid, parent_id):
221             """Add a file to the inventory."""
222             if os.path.islink(self.abspath(relpath)):
223                 file = InventoryLink(id, os.path.basename(relpath), parent_id)
224                 file.revision = revid
225                 file.symlink_target = os.readlink(self.abspath(relpath))
226                 file.text_sha1 = None
227                 file.text_size = None
228                 file.executable = False
229                 inv.add(file)
230             else:
231                 file = InventoryFile(id, os.path.basename(relpath), parent_id)
232                 file.revision = revid
233                 try:
234                     data = fingerprint_file(open(self.abspath(relpath)))
235                     file.text_sha1 = data['sha1']
236                     file.text_size = data['size']
237                     file.executable = self.is_executable(id, relpath)
238                     inv.add(file)
239                 except IOError:
240                     # Ignore non-existing files
241                     pass
242
243         def find_copies(url, relpath=""):
244             wc = self._get_wc(relpath)
245             entries = svn.wc.entries_read(wc, False)
246             for entry in entries.values():
247                 subrelpath = os.path.join(relpath, entry.name)
248                 if entry.name == "" or entry.kind != 'directory':
249                     if ((entry.copyfrom_url == url or entry.url == url) and 
250                         not (entry.schedule in (svn.wc.schedule_delete,
251                                                 svn.wc.schedule_replace))):
252                         yield os.path.join(
253                                 self.branch.branch_path.strip("/"), 
254                                 subrelpath)
255                 else:
256                     find_copies(subrelpath)
257             svn.wc.adm_close(wc)
258
259         def find_ids(entry, rootwc):
260             relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
261             assert entry.schedule in (svn.wc.schedule_normal, 
262                                       svn.wc.schedule_delete,
263                                       svn.wc.schedule_add,
264                                       svn.wc.schedule_replace)
265             if entry.schedule == svn.wc.schedule_normal:
266                 assert entry.revision >= 0
267                 # Keep old id
268                 return self.path_to_file_id(entry.cmt_rev, entry.revision, 
269                         relpath)
270             elif entry.schedule == svn.wc.schedule_delete:
271                 return (None, None)
272             elif (entry.schedule == svn.wc.schedule_add or 
273                   entry.schedule == svn.wc.schedule_replace):
274                 # See if the file this file was copied from disappeared
275                 # and has no other copies -> in that case, take id of other file
276                 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
277                     return self.path_to_file_id(entry.copyfrom_rev, entry.revision,
278                             entry.copyfrom_url[len(entry.repos):])
279                 ids = self._get_new_file_ids(rootwc)
280                 if ids.has_key(relpath):
281                     return (ids[relpath], None)
282                 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
283
284         def add_dir_to_inv(relpath, wc, parent_id):
285             entries = svn.wc.entries_read(wc, False)
286             entry = entries[""]
287             (id, revid) = find_ids(entry, rootwc)
288             if id is None:
289                 mutter('no id for %r' % entry.url)
290                 return
291
292             # First handle directory itself
293             if relpath == "":
294                 inv.add_path("", 'directory', ROOT_ID)
295                 inv.revision_id = revid
296             else:
297                 inventry = InventoryDirectory(id, os.path.basename(relpath), parent_id)
298                 inventry.revision = revid
299                 inv.add(inventry)
300
301             for name in entries:
302                 if name == "":
303                     continue
304
305                 subrelpath = os.path.join(relpath, name)
306
307                 entry = entries[name]
308                 assert entry
309                 
310                 if entry.kind == svn.core.svn_node_dir:
311                     subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath), 
312                                              False, 0, None)
313                     try:
314                         add_dir_to_inv(subrelpath, subwc, id)
315                     finally:
316                         svn.wc.adm_close(subwc)
317                 else:
318                     (subid, subrevid) = find_ids(entry, rootwc)
319                     if subid:
320                         add_file_to_inv(subrelpath, subid, subrevid, id)
321                     else:
322                         mutter('no id for %r' % entry.url)
323
324         rootwc = self._get_wc() 
325         try:
326             add_dir_to_inv("", rootwc, None)
327         finally:
328             svn.wc.adm_close(rootwc)
329
330         self._set_inventory(inv, dirty=False)
331         return inv
332
333     def set_last_revision(self, revid):
334         mutter('setting last revision to %r' % revid)
335         if revid is None or revid == NULL_REVISION:
336             self.base_revid = revid
337             self.base_revnum = 0
338             self.base_tree = RevisionTree(self, Inventory(), revid)
339             return
340
341         (bp, rev) = self.branch.repository.parse_revision_id(revid)
342         assert bp == self.branch.branch_path
343         self.base_revnum = rev
344         self.base_revid = revid
345         self.base_tree = SvnBasisTree(self)
346
347         # TODO: Implement more efficient version
348         newrev = self.branch.repository.get_revision(revid)
349         newrevtree = self.branch.repository.revision_tree(revid)
350
351         def update_settings(wc, path):
352             id = newrevtree.inventory.path2id(path)
353             mutter("Updating settings for %r" % id)
354             (_, revnum) = self.branch.repository.parse_revision_id(
355                     newrevtree.inventory[id].revision)
356
357             svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc, 
358                           False, revnum, 
359                           svn.core.svn_time_to_cstring(newrev.timestamp), 
360                           newrev.committer, None, False)
361
362             if newrevtree.inventory[id].kind != 'directory':
363                 return
364
365             entries = svn.wc.entries_read(wc, True)
366             for entry in entries:
367                 if entry == "":
368                     continue
369
370                 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
371                 try:
372                     update_settings(subwc, os.path.join(path, entry))
373                 finally:
374                     svn.wc.adm_close(subwc)
375
376         # Set proper version for all files in the wc
377         wc = self._get_wc(write_lock=True)
378         try:
379             update_settings(wc, "")
380         finally:
381             svn.wc.adm_close(wc)
382         self.base_revid = revid
383
384     def commit(self, message=None, message_callback=None, revprops=None, timestamp=None, timezone=None, committer=None, 
385                rev_id=None, allow_pointless=True, strict=False, verbose=False, local=False, reporter=None, config=None, 
386                specific_files=None):
387         assert timestamp is None
388         assert timezone is None
389         assert rev_id is None
390
391         if specific_files:
392             specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
393         else:
394             specific_files = [self.basedir.encode('utf8')]
395
396         if message_callback is not None:
397             def log_message_func(items, pool):
398                 """ Simple log message provider for unit tests. """
399                 return str(message_callback(self))
400         else:
401             assert isinstance(message, basestring)
402             def log_message_func(items, pool):
403                 """ Simple log message provider for unit tests. """
404                 return str(message)
405
406         self.client_ctx.log_msg_baton2 = log_message_func
407         commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
408         self.client_ctx.log_msg_baton2 = None
409
410         revid = self.branch.repository.generate_revision_id(
411                 commit_info.revision, self.branch.branch_path)
412
413         self.base_revid = revid
414         self.base_revnum = commit_info.revision
415         self.base_tree = SvnBasisTree(self)
416
417         #FIXME: Use public API:
418         self.branch.revision_history()
419         self.branch._revision_history.append(revid)
420
421         return revid
422
423     def add(self, files, ids=None):
424         import pdb
425         pdb.set_trace()
426         if ids:
427             ids = copy(ids)
428             ids.reverse()
429         assert isinstance(files, list)
430         for f in files:
431             try:
432                 wc = self._get_wc(os.path.dirname(f), write_lock=True)
433                 try:
434                     svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0, 
435                             None, None, None)
436                     if ids:
437                         self._change_fileid_mapping(ids.pop(), f, wc)
438                 except SubversionException, (_, num):
439                     if num == svn.core.SVN_ERR_ENTRY_EXISTS:
440                         continue
441                     elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
442                         raise NoSuchFile(path=f)
443                     raise
444             finally:
445                 svn.wc.adm_close(wc)
446         self.read_working_inventory()
447
448     def basis_tree(self):
449         if self.base_revid is None or self.base_revid == NULL_REVISION:
450             return self.branch.repository.revision_tree(self.base_revid)
451
452         return self.base_tree
453
454     def pull(self, source, overwrite=False, stop_revision=None):
455         if stop_revision is None:
456             stop_revision = self.branch.last_revision()
457         rev = svn.core.svn_opt_revision_t()
458         rev.kind = svn.core.svn_opt_revision_number
459         rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
460         fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
461         self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
462         return fetched-rev.value.number
463
464     def get_file_sha1(self, file_id, path=None, stat_value=None):
465         if not path:
466             path = self._inventory.id2path(file_id)
467         return fingerprint_file(open(self.abspath(path)))['sha1']
468
469     def _change_fileid_mapping(self, id, path, wc=None):
470         if wc is None:
471             subwc = self._get_wc(write_lock=True)
472         else:
473             subwc = wc
474         new_entries = self._get_new_file_ids(subwc)
475         if id is None:
476             if new_entries.has_key(path):
477                 del new_entries[path]
478         else:
479             new_entries[path] = id
480         committed = self.branch.repository.branchprop_list.get_property(
481                 self.branch.branch_path, 
482                 self.base_revnum, 
483                 SVN_PROP_BZR_FILEIDS, "")
484         existing = committed + "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
485         if existing != "":
486             svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, str(existing), self.basedir, subwc)
487         if wc is None:
488             svn.wc.adm_close(subwc)
489
490     def _get_new_file_ids(self, wc):
491         committed = self.branch.repository.branchprop_list.get_property(
492                 self.branch.branch_path, 
493                 self.base_revnum, 
494                 SVN_PROP_BZR_FILEIDS, "")
495         existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
496         if existing is None:
497             return {}
498         else:
499             return dict(map(lambda x: x.split("\t"), existing[len(committed):].splitlines()))
500
501     def _get_bzr_merges(self):
502         return self.branch.repository.branchprop_list.get_property(
503                 self.branch.branch_path, 
504                 self.base_revnum, 
505                 SVN_PROP_BZR_MERGE, "")
506
507     def _get_svk_merges(self):
508         return self.branch.repository.branchprop_list.get_property(
509                 self.branch.branch_path, 
510                 self.base_revnum, 
511                 SVN_PROP_SVK_MERGE, "")
512
513     def set_pending_merges(self, merges):
514         wc = self._get_wc(write_lock=True)
515         try:
516             # Set bzr:merge
517             if len(merges) > 0:
518                 bzr_merge = "\t".join(merges) + "\n"
519             else:
520                 bzr_merge = ""
521
522             svn.wc.prop_set(SVN_PROP_BZR_MERGE, 
523                                  self._get_bzr_merges() + bzr_merge, 
524                                  self.basedir, wc)
525
526             # Set svk:merge
527             svk_merge = ""
528             for merge in merges:
529                 try:
530                     svk_merge += revision_id_to_svk_feature(merge) + "\n"
531                 except InvalidRevisionId:
532                     pass
533
534             svn.wc.prop_set2(SVN_PROP_SVK_MERGE, 
535                              self._get_svk_merges() + svk_merge, self.basedir, 
536                              wc, False)
537         finally:
538             svn.wc.adm_close(wc)
539
540     def add_pending_merge(self, revid):
541         merges = self.pending_merges()
542         merges.append(revid)
543         self.set_pending_merges(existing)
544
545     def pending_merges(self):
546         merged = self._get_bzr_merges().splitlines()
547         wc = self._get_wc()
548         try:
549             merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
550             if merged_data is None:
551                 set_merged = []
552             else:
553                 set_merged = merged_data.splitlines()
554         finally:
555             svn.wc.adm_close(wc)
556
557         assert (len(merged) == len(set_merged) or 
558                len(merged)+1 == len(set_merged))
559
560         if len(set_merged) > len(merged):
561             return set_merged[-1].split("\t")
562
563         return []
564
565
566 class SvnWorkingTreeFormat(WorkingTreeFormat):
567     def get_format_description(self):
568         return "Subversion Working Copy"
569
570     def initialize(self, a_bzrdir, revision_id=None):
571         raise NotImplementedError(self.initialize)
572
573     def open(self, a_bzrdir):
574         raise NotImplementedError(self.initialize)
575
576
577 class SvnCheckout(BzrDir):
578     """BzrDir implementation for Subversion checkouts (directories 
579     containing a .svn subdirectory."""
580     def __init__(self, transport, format):
581         super(SvnCheckout, self).__init__(transport, format)
582         self.local_path = transport.local_abspath(".")
583         
584         # Open related remote repository + branch
585         wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
586         try:
587             svn_url = svn.wc.entry(self.local_path, wc, True).url
588         finally:
589             svn.wc.adm_close(wc)
590
591         self.remote_transport = SvnRaTransport(svn_url)
592         self.svn_root_transport = SvnRaTransport(self.remote_transport.get_repos_root())
593         self.root_transport = self.transport = transport
594
595         self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
596         self.scheme = BranchingScheme.guess_scheme(self.branch_path)
597         mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
598         if not self.scheme.is_branch(self.branch_path):
599             raise NotBranchError(path=self.transport.base)
600
601     def clone(self, path):
602         raise NotImplementedError(self.clone)
603
604     def open_workingtree(self, _unsupported=False):
605         return SvnWorkingTree(self, self.local_path, self.open_branch())
606
607     def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
608         # FIXME: honor force_new_repo
609         result = BzrDirFormat.get_default_format().initialize(url)
610         repo = self.find_repository()
611         result_repo = repo.clone(result, revision_id, basis)
612         branch = self.open_branch()
613         branch.sprout(result, revision_id)
614         result.create_workingtree()
615         return result
616
617     def open_repository(self):
618         raise NoRepositoryPresent(self)
619
620     def find_repository(self):
621         return SvnRepository(self, self.svn_root_transport)
622
623     def create_workingtree(self, revision_id=None):
624         """See BzrDir.create_workingtree().
625
626         Not implemented for Subversion because having a .svn directory
627         implies having a working copy.
628         """
629         raise NotImplementedError(self.create_workingtree)
630
631     def create_branch(self):
632         """See BzrDir.create_branch()."""
633         raise NotImplementedError(self.create_branch)
634
635     def open_branch(self, unsupported=True):
636         """See BzrDir.open_branch()."""
637         repos = self.find_repository()
638
639         try:
640             branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
641         except SubversionException, (msg, num):
642             if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
643                raise NotBranchError(path=self.url)
644             raise
645  
646         branch.bzrdir = self
647         return branch
648
649
650 class SvnWorkingTreeDirFormat(BzrDirFormat):
651     """Working Tree implementation that uses Subversion working copies."""
652     _lock_class = TransportLock
653
654     @classmethod
655     def probe_transport(klass, transport):
656         format = klass()
657
658         if isinstance(transport, LocalTransport) and \
659             transport.has(svn.wc.get_adm_dir()):
660             return format
661
662         raise NotBranchError(path=transport.base)
663
664     def _open(self, transport):
665         return SvnCheckout(transport, self)
666
667     def get_format_string(self):
668         return 'Subversion Local Checkout'
669
670     def get_format_description(self):
671         return 'Subversion Local Checkout'
672
673     def initialize_on_transport(self, transport):
674         raise NotImplementedError(self.initialize_on_transport)