1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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)."""
19 from bzrlib import urlutils
20 from bzrlib.branch import PullResult
21 from bzrlib.bzrdir import BzrDirFormat, BzrDir
22 from bzrlib.errors import (InvalidRevisionId, NotBranchError, NoSuchFile,
23 NoRepositoryPresent, BzrError, UninitializableFormat)
24 from bzrlib.inventory import Inventory, InventoryFile, InventoryLink
25 from bzrlib.lockable_files import TransportLock, LockableFiles
26 from bzrlib.lockdir import LockDir
27 from bzrlib.osutils import file_kind, fingerprint_file
28 from bzrlib.revision import NULL_REVISION
29 from bzrlib.trace import mutter
30 from bzrlib.tree import RevisionTree
31 from bzrlib.transport.local import LocalTransport
32 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
34 from branch import SvnBranch
35 from convert import SvnConverter
36 from errors import LocalCommitsUnsupported, NoSvnRepositoryPresent
37 from remote import SvnRemoteAccess
38 from repository import (SvnRepository, SVN_PROP_BZR_ANCESTRY,
39 SVN_PROP_SVK_MERGE, SVN_PROP_BZR_FILEIDS,
40 SVN_PROP_BZR_REVISION_ID, SVN_PROP_BZR_REVISION_INFO,
41 revision_id_to_svk_feature, generate_revision_metadata)
42 from revids import escape_svn_path
43 from scheme import BranchingScheme
44 from transport import (SvnRaTransport, bzr_to_svn_url, create_svn_client,
46 from tree import SvnBasisTree
51 import svn.core, svn.wc
52 from svn.core import SubversionException, Pool
54 from errors import NoCheckoutSupport
55 from format import get_rich_root_format
57 class WorkingTreeInconsistent(BzrError):
58 _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
60 def __init__(self, min_revnum, max_revnum):
61 self.min_revnum = min_revnum
62 self.max_revnum = max_revnum
65 class SvnWorkingTree(WorkingTree):
66 """WorkingTree implementation that uses a Subversion Working Copy for storage."""
67 def __init__(self, bzrdir, local_path, branch):
68 self._format = SvnWorkingTreeFormat()
69 self.basedir = local_path
74 self.client_ctx = create_svn_client(self.pool)
75 self.client_ctx.log_msg_func2 = \
76 svn.client.svn_swig_py_get_commit_log_func
79 status = svn.wc.revision_status(self.basedir, None, True, None, None)
80 if status.min_rev != status.max_rev:
81 #raise WorkingTreeInconsistent(status.min_rev, status.max_rev)
82 rev = svn.core.svn_opt_revision_t()
83 rev.kind = svn.core.svn_opt_revision_number
84 rev.value.number = status.max_rev
85 assert status.max_rev == svn.client.update(self.basedir, rev,
86 True, self.client_ctx, Pool())
88 self.base_revnum = status.max_rev
89 self.base_tree = SvnBasisTree(self)
90 self.base_revid = branch.generate_revision_id(self.base_revnum)
92 self.read_working_inventory()
94 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(),
97 os.makedirs(self.controldir)
98 os.makedirs(os.path.join(self.controldir, 'lock'))
101 control_transport = bzrdir.transport.clone(os.path.join(
102 svn.wc.get_adm_dir(), 'bzr'))
103 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
105 def get_ignore_list(self):
106 ignores = set([svn.wc.get_adm_dir()])
107 ignores.update(svn.wc.get_default_ignores(svn_config))
109 def dir_add(wc, prefix):
110 ignorestr = svn.wc.prop_get(svn.core.SVN_PROP_IGNORE,
111 self.abspath(prefix).rstrip("/"), wc)
112 if ignorestr is not None:
113 for pat in ignorestr.splitlines():
114 ignores.add("./"+os.path.join(prefix, pat))
116 entries = svn.wc.entries_read(wc, False)
117 for entry in entries:
121 if entries[entry].kind != svn.core.svn_node_dir:
124 subprefix = os.path.join(prefix, entry)
126 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False,
129 dir_add(subwc, subprefix)
131 svn.wc.adm_close(subwc)
141 def is_control_filename(self, path):
142 return svn.wc.is_adm_dir(path)
144 def apply_inventory_delta(self, changes):
145 raise NotImplementedError(self.apply_inventory_delta)
147 def update(self, change_reporter=None):
148 rev = svn.core.svn_opt_revision_t()
149 rev.kind = svn.core.svn_opt_revision_head
150 svn.client.update(self.basedir, rev, True, self.client_ctx)
152 def remove(self, files, verbose=False, to_file=None):
153 # FIXME: Use to_file argument
154 # FIXME: Use verbose argument
155 assert isinstance(files, list)
156 wc = self._get_wc(write_lock=True)
159 svn.wc.delete2(self.abspath(file), wc, None, None, None)
164 self._change_fileid_mapping(None, file)
165 self.read_working_inventory()
167 def _get_wc(self, relpath="", write_lock=False):
168 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"),
171 def _get_rel_wc(self, relpath, write_lock=False):
172 dir = os.path.dirname(relpath)
173 file = os.path.basename(relpath)
174 return (self._get_wc(dir, write_lock), file)
176 def move(self, from_paths, to_dir=None, after=False, **kwargs):
177 # FIXME: Use after argument
179 revt = svn.core.svn_opt_revision_t()
180 revt.kind = svn.core.svn_opt_revision_working
181 for entry in from_paths:
183 to_wc = self._get_wc(to_dir, write_lock=True)
184 svn.wc.copy(self.abspath(entry), to_wc,
185 os.path.basename(entry), None, None)
187 svn.wc.adm_close(to_wc)
189 from_wc = self._get_wc(write_lock=True)
190 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
192 svn.wc.adm_close(from_wc)
193 new_name = urlutils.join(to_dir, os.path.basename(entry))
194 self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
195 self._change_fileid_mapping(None, entry)
197 self.read_working_inventory()
199 def rename_one(self, from_rel, to_rel, after=False):
202 revt = svn.core.svn_opt_revision_t()
203 revt.kind = svn.core.svn_opt_revision_unspecified
204 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
205 if os.path.dirname(from_rel) == os.path.dirname(to_rel):
206 # Prevent lock contention
209 (from_wc, _) = self._get_rel_wc(from_rel, write_lock=True)
210 from_id = self.inventory.path2id(from_rel)
212 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
213 svn.wc.delete2(self.abspath(from_rel), from_wc, None, None, None)
215 svn.wc.adm_close(to_wc)
216 self._change_fileid_mapping(None, from_rel)
217 self._change_fileid_mapping(from_id, to_rel)
218 self.read_working_inventory()
220 def path_to_file_id(self, revnum, current_revnum, path):
221 """Generate a bzr file id from a Subversion file name.
223 :param revnum: Revision number.
224 :param path: Absolute path.
225 :return: Tuple with file id and revision id.
227 assert isinstance(revnum, int) and revnum >= 0
228 assert isinstance(path, basestring)
230 (_, rp) = self.branch.scheme.unprefix(path)
231 entry = self.base_tree.id_map[rp]
232 assert entry[0] is not None
233 assert isinstance(entry[0], str), "fileid %r for %r is not a string" % (entry[0], path)
236 def read_working_inventory(self):
239 def add_file_to_inv(relpath, id, revid, parent_id):
240 """Add a file to the inventory."""
241 if os.path.islink(self.abspath(relpath)):
242 file = InventoryLink(id, os.path.basename(relpath), parent_id)
243 file.revision = revid
244 file.symlink_target = os.readlink(self.abspath(relpath))
245 file.text_sha1 = None
246 file.text_size = None
247 file.executable = False
250 file = InventoryFile(id, os.path.basename(relpath), parent_id)
251 file.revision = revid
253 data = fingerprint_file(open(self.abspath(relpath)))
254 file.text_sha1 = data['sha1']
255 file.text_size = data['size']
256 file.executable = self.is_executable(id, relpath)
259 # Ignore non-existing files
262 def find_copies(url, relpath=""):
263 wc = self._get_wc(relpath)
264 entries = svn.wc.entries_read(wc, False)
265 for entry in entries.values():
266 subrelpath = os.path.join(relpath, entry.name)
267 if entry.name == "" or entry.kind != 'directory':
268 if ((entry.copyfrom_url == url or entry.url == url) and
269 not (entry.schedule in (svn.wc.schedule_delete,
270 svn.wc.schedule_replace))):
272 self.branch.get_branch_path().strip("/"),
275 find_copies(subrelpath)
278 def find_ids(entry, rootwc):
279 relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
280 assert entry.schedule in (svn.wc.schedule_normal,
281 svn.wc.schedule_delete,
283 svn.wc.schedule_replace)
284 if entry.schedule == svn.wc.schedule_normal:
285 assert entry.revision >= 0
287 return self.path_to_file_id(entry.cmt_rev, entry.revision,
289 elif entry.schedule == svn.wc.schedule_delete:
291 elif (entry.schedule == svn.wc.schedule_add or
292 entry.schedule == svn.wc.schedule_replace):
293 # See if the file this file was copied from disappeared
294 # and has no other copies -> in that case, take id of other file
295 if (entry.copyfrom_url and
296 list(find_copies(entry.copyfrom_url)) == [relpath]):
297 return self.path_to_file_id(entry.copyfrom_rev,
298 entry.revision, entry.copyfrom_url[len(entry.repos):])
299 ids = self._get_new_file_ids(rootwc)
300 if ids.has_key(relpath):
301 return (ids[relpath], None)
302 # FIXME: Generate more random file ids
303 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
305 def add_dir_to_inv(relpath, wc, parent_id):
306 entries = svn.wc.entries_read(wc, False)
308 assert parent_id is None or isinstance(parent_id, str), \
309 "%r is not a string" % parent_id
310 (id, revid) = find_ids(entry, rootwc)
312 mutter('no id for %r' % entry.url)
314 assert revid is None or isinstance(revid, str), "%r is not a string" % revid
315 assert isinstance(id, str), "%r is not a string" % id
317 # First handle directory itself
318 inv.add_path(relpath, 'directory', id, parent_id).revision = revid
320 inv.revision_id = revid
326 subrelpath = os.path.join(relpath, name)
328 entry = entries[name]
331 if entry.kind == svn.core.svn_node_dir:
332 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
335 add_dir_to_inv(subrelpath, subwc, id)
337 svn.wc.adm_close(subwc)
339 (subid, subrevid) = find_ids(entry, rootwc)
341 add_file_to_inv(subrelpath, subid, subrevid, id)
343 mutter('no id for %r' % entry.url)
345 rootwc = self._get_wc()
347 add_dir_to_inv("", rootwc, None)
349 svn.wc.adm_close(rootwc)
351 self._set_inventory(inv, dirty=False)
354 def set_last_revision(self, revid):
355 mutter('setting last revision to %r' % revid)
356 if revid is None or revid == NULL_REVISION:
357 self.base_revid = revid
359 self.base_tree = RevisionTree(self, Inventory(), revid)
362 rev = self.branch.lookup_revision_id(revid)
363 self.base_revnum = rev
364 self.base_revid = revid
365 self.base_tree = SvnBasisTree(self)
367 # TODO: Implement more efficient version
368 newrev = self.branch.repository.get_revision(revid)
369 newrevtree = self.branch.repository.revision_tree(revid)
371 def update_settings(wc, path):
372 id = newrevtree.inventory.path2id(path)
373 mutter("Updating settings for %r" % id)
374 revnum = self.branch.lookup_revision_id(
375 newrevtree.inventory[id].revision)
377 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
379 svn.core.svn_time_to_cstring(newrev.timestamp),
380 newrev.committer, None, False)
382 if newrevtree.inventory[id].kind != 'directory':
385 entries = svn.wc.entries_read(wc, True)
386 for entry in entries:
390 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
392 update_settings(subwc, os.path.join(path, entry))
394 svn.wc.adm_close(subwc)
396 # Set proper version for all files in the wc
397 wc = self._get_wc(write_lock=True)
399 update_settings(wc, "")
402 self.base_revid = revid
404 def commit(self, message=None, message_callback=None, revprops=None,
405 timestamp=None, timezone=None, committer=None, rev_id=None,
406 allow_pointless=True, strict=False, verbose=False, local=False,
407 reporter=None, config=None, specific_files=None, author=None):
408 if author is not None:
409 revprops['author'] = author
410 # FIXME: Use allow_pointless
412 # FIXME: Use reporter
415 raise LocalCommitsUnsupported()
418 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
420 specific_files = [self.basedir.encode('utf8')]
422 if message_callback is not None:
423 def log_message_func(items, pool):
424 """ Simple log message provider for unit tests. """
425 return message_callback(self).encode("utf-8")
427 assert isinstance(message, basestring)
428 def log_message_func(items, pool):
429 """ Simple log message provider for unit tests. """
430 return message.encode("utf-8")
432 self.client_ctx.log_msg_baton2 = log_message_func
433 if rev_id is not None:
434 extra = "%d %s\n" % (self.branch.revno()+1, rev_id)
437 wc = self._get_wc(write_lock=True)
439 svn.wc.prop_set(SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme),
440 self._get_bzr_revids() + extra,
442 svn.wc.prop_set(SVN_PROP_BZR_REVISION_INFO,
443 generate_revision_metadata(timestamp,
452 commit_info = svn.client.commit3(specific_files, True, False,
455 # Reset properties so the next subversion commit won't
456 # accidently set these properties.
457 wc = self._get_wc(write_lock=True)
458 svn.wc.prop_set(SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme),
459 self._get_bzr_revids(), self.basedir, wc)
460 svn.wc.prop_set(SVN_PROP_BZR_REVISION_INFO,
461 self.branch.repository.branchprop_list.get_property(
462 self.branch.get_branch_path(self.base_revnum),
464 SVN_PROP_BZR_REVISION_INFO, ""),
469 self.client_ctx.log_msg_baton2 = None
471 revid = self.branch.generate_revision_id(commit_info.revision)
473 self.base_revid = revid
474 self.base_revnum = commit_info.revision
475 self.base_tree = SvnBasisTree(self)
479 def smart_add(self, file_list, recurse=True, action=None, save=True):
480 assert isinstance(recurse, bool)
482 action = bzrlib.add.AddAction()
485 # no paths supplied: add the entire tree.
490 for file_path in file_list:
492 file_path = os.path.abspath(file_path)
493 f = self.relpath(file_path)
494 wc = self._get_wc(os.path.dirname(f), write_lock=True)
496 if not self.inventory.has_filename(f):
498 mutter('adding %r' % file_path)
499 svn.wc.add2(file_path, wc, None, 0, None, None, None)
500 added.append(file_path)
501 if recurse and file_kind(file_path) == 'directory':
502 # Filter out ignored files and update ignored
503 for c in os.listdir(file_path):
504 if self.is_control_filename(c):
506 c_path = os.path.join(file_path, c)
507 ignore_glob = self.is_ignored(c)
508 if ignore_glob is not None:
509 ignored.setdefault(ignore_glob, []).append(c_path)
514 cadded, cignored = self.smart_add(todo, recurse, action, save)
516 ignored.update(cignored)
517 return added, ignored
519 def add(self, files, ids=None, kinds=None):
521 if isinstance(files, str):
523 if isinstance(ids, str):
527 assert isinstance(files, list)
529 wc = self._get_wc(os.path.dirname(f), write_lock=True)
532 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
535 self._change_fileid_mapping(ids.next(), f, wc)
536 except SubversionException, (_, num):
537 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
539 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
540 raise NoSuchFile(path=f)
544 self.read_working_inventory()
546 def basis_tree(self):
547 if self.base_revid is None or self.base_revid == NULL_REVISION:
548 return self.branch.repository.revision_tree(self.base_revid)
550 return self.base_tree
552 def pull(self, source, overwrite=False, stop_revision=None,
553 delta_reporter=None):
554 # FIXME: Use delta_reporter
555 # FIXME: Use overwrite
556 result = PullResult()
557 result.source_branch = source
558 result.master_branch = None
559 result.target_branch = self.branch
560 (result.old_revno, result.old_revid) = self.branch.last_revision_info()
561 if stop_revision is None:
562 stop_revision = self.branch.last_revision()
563 rev = svn.core.svn_opt_revision_t()
564 rev.kind = svn.core.svn_opt_revision_number
565 rev.value.number = self.branch.lookup_revision_id(stop_revision)
566 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
567 self.base_revid = self.branch.generate_revision_id(fetched)
568 result.new_revid = self.base_revid
569 result.new_revno = self.branch.revision_id_to_revno(result.new_revid)
572 def get_file_sha1(self, file_id, path=None, stat_value=None):
574 path = self._inventory.id2path(file_id)
575 return fingerprint_file(open(self.abspath(path)))['sha1']
577 def _change_fileid_mapping(self, id, path, wc=None):
579 subwc = self._get_wc(write_lock=True)
582 new_entries = self._get_new_file_ids(subwc)
584 if new_entries.has_key(path):
585 del new_entries[path]
587 assert isinstance(id, str)
588 new_entries[path] = id
589 existing = "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
591 svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, existing.encode("utf-8"), self.basedir, subwc)
593 svn.wc.adm_close(subwc)
595 def _get_new_file_ids(self, wc):
596 committed = self.branch.repository.branchprop_list.get_property(
597 self.branch.get_branch_path(self.base_revnum), self.base_revnum,
598 SVN_PROP_BZR_FILEIDS, "")
599 existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
600 if existing is None or committed == existing:
602 return dict(map(lambda x: str(x).split("\t"),
603 existing.splitlines()))
605 def _get_bzr_revids(self):
606 return self.branch.repository.branchprop_list.get_property(
607 self.branch.get_branch_path(self.base_revnum),
609 SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), "")
611 def _get_bzr_merges(self):
612 return self.branch.repository.branchprop_list.get_property(
613 self.branch.get_branch_path(self.base_revnum),
614 self.base_revnum, SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), "")
616 def _get_svk_merges(self):
617 return self.branch.repository.branchprop_list.get_property(
618 self.branch.get_branch_path(self.base_revnum),
619 self.base_revnum, SVN_PROP_SVK_MERGE, "")
621 def set_pending_merges(self, merges):
622 """See MutableTree.set_pending_merges()."""
623 wc = self._get_wc(write_lock=True)
627 bzr_merge = "\t".join(merges) + "\n"
631 svn.wc.prop_set(SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme),
632 self._get_bzr_merges() + bzr_merge,
639 svk_merge += revision_id_to_svk_feature(merge) + "\n"
640 except InvalidRevisionId:
643 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
644 self._get_svk_merges() + svk_merge, self.basedir,
649 def add_pending_merge(self, revid):
650 merges = self.pending_merges()
652 self.set_pending_merges(merges)
654 def pending_merges(self):
655 merged = self._get_bzr_merges().splitlines()
658 merged_data = svn.wc.prop_get(
659 SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), self.basedir, wc)
660 if merged_data is None:
663 set_merged = merged_data.splitlines()
667 assert (len(merged) == len(set_merged) or
668 len(merged)+1 == len(set_merged))
670 if len(set_merged) > len(merged):
671 return set_merged[-1].split("\t")
675 def _reset_data(self):
679 # reverse order of locking.
681 return self._control_files.unlock()
687 class SvnWorkingTreeFormat(WorkingTreeFormat):
688 """Subversion working copy format."""
689 def __get_matchingbzrdir(self):
690 return SvnWorkingTreeDirFormat()
692 _matchingbzrdir = property(__get_matchingbzrdir)
694 def get_format_description(self):
695 return "Subversion Working Copy"
697 def get_format_string(self):
698 return "Subversion Working Copy Format"
700 def initialize(self, a_bzrdir, revision_id=None):
701 raise NotImplementedError(self.initialize)
703 def open(self, a_bzrdir):
704 raise NotImplementedError(self.initialize)
707 class SvnCheckout(BzrDir):
708 """BzrDir implementation for Subversion checkouts (directories
709 containing a .svn subdirectory."""
710 def __init__(self, transport, format):
711 super(SvnCheckout, self).__init__(transport, format)
712 self.local_path = transport.local_abspath(".")
714 # Open related remote repository + branch
715 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
717 svn_url = svn.wc.entry(self.local_path, wc, True).url
721 remote_transport = SvnRaTransport(svn_url)
722 self.remote_bzrdir = SvnRemoteAccess(remote_transport)
723 self.svn_root_transport = remote_transport.clone_root()
724 self.root_transport = self.transport = transport
726 def clone(self, path, revision_id=None, force_new_repo=False):
727 raise NotImplementedError(self.clone)
729 def open_workingtree(self, _unsupported=False, recommend_upgrade=False):
730 return SvnWorkingTree(self, self.local_path, self.open_branch())
732 def sprout(self, url, revision_id=None, force_new_repo=False,
733 recurse='down', possible_transports=None):
734 # FIXME: honor force_new_repo
736 result = get_rich_root_format().initialize(url)
737 repo = self.find_repository()
738 repo.clone(result, revision_id)
739 branch = self.open_branch()
740 branch.sprout(result, revision_id)
741 result.create_workingtree()
744 def open_repository(self):
745 raise NoRepositoryPresent(self)
747 def find_repository(self):
748 return SvnRepository(self, self.svn_root_transport, self.remote_bzrdir.branch_path)
750 def needs_format_conversion(self, format=None):
752 format = BzrDirFormat.get_default_format()
753 return not isinstance(self._format, format.__class__)
755 def create_workingtree(self, revision_id=None):
756 """See BzrDir.create_workingtree().
758 Not implemented for Subversion because having a .svn directory
759 implies having a working copy.
761 raise NotImplementedError(self.create_workingtree)
763 def create_branch(self):
764 """See BzrDir.create_branch()."""
765 raise NotImplementedError(self.create_branch)
767 def open_branch(self, unsupported=True):
768 """See BzrDir.open_branch()."""
769 repos = self.find_repository()
772 branch = SvnBranch(self.svn_root_transport.base, repos,
773 self.remote_bzrdir.branch_path)
774 except SubversionException, (_, num):
775 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
776 raise NotBranchError(path=self.base)
779 branch.bzrdir = self.remote_bzrdir