Update installation requirements.
[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 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 """Checkouts and working trees (working copies)."""
17
18 import bzrlib
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
33
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,
45                        svn_config) 
46 from tree import SvnBasisTree
47
48 import os
49 import urllib
50
51 import svn.core, svn.wc
52 from svn.core import SubversionException, Pool
53
54 from errors import NoCheckoutSupport
55 from format import get_rich_root_format
56
57 class WorkingTreeInconsistent(BzrError):
58     _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
59
60     def __init__(self, min_revnum, max_revnum):
61         self.min_revnum = min_revnum
62         self.max_revnum = max_revnum
63
64
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
70         self.bzrdir = bzrdir
71         self._branch = branch
72         self.base_revnum = 0
73         self.pool = Pool()
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
77
78         self._get_wc()
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())
87
88         self.base_revnum = status.max_rev
89         self.base_tree = SvnBasisTree(self)
90         self.base_revid = branch.generate_revision_id(self.base_revnum)
91
92         self.read_working_inventory()
93
94         self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 
95                                        'bzr')
96         try:
97             os.makedirs(self.controldir)
98             os.makedirs(os.path.join(self.controldir, 'lock'))
99         except OSError:
100             pass
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)
104
105     def get_ignore_list(self):
106         ignores = set([svn.wc.get_adm_dir()])
107         ignores.update(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, 
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))
115
116             entries = svn.wc.entries_read(wc, False)
117             for entry in entries:
118                 if entry == "":
119                     continue
120
121                 if entries[entry].kind != svn.core.svn_node_dir:
122                     continue
123
124                 subprefix = os.path.join(prefix, entry)
125
126                 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 
127                                          0, None)
128                 try:
129                     dir_add(subwc, subprefix)
130                 finally:
131                     svn.wc.adm_close(subwc)
132
133         wc = self._get_wc()
134         try:
135             dir_add(wc, "")
136         finally:
137             svn.wc.adm_close(wc)
138
139         return ignores
140
141     def is_control_filename(self, path):
142         return svn.wc.is_adm_dir(path)
143
144     def apply_inventory_delta(self, changes):
145         raise NotImplementedError(self.apply_inventory_delta)
146
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)
151
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)
157         try:
158             for file in files:
159                 svn.wc.delete2(self.abspath(file), wc, None, None, None)
160         finally:
161             svn.wc.adm_close(wc)
162
163         for file in files:
164             self._change_fileid_mapping(None, file)
165         self.read_working_inventory()
166
167     def _get_wc(self, relpath="", write_lock=False):
168         return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), 
169                                 write_lock, 0, None)
170
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)
175
176     def move(self, from_paths, to_dir=None, after=False, **kwargs):
177         # FIXME: Use after argument
178         assert after != True
179         revt = svn.core.svn_opt_revision_t()
180         revt.kind = svn.core.svn_opt_revision_working
181         for entry in from_paths:
182             try:
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)
186             finally:
187                 svn.wc.adm_close(to_wc)
188             try:
189                 from_wc = self._get_wc(write_lock=True)
190                 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
191             finally:
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)
196
197         self.read_working_inventory()
198
199     def rename_one(self, from_rel, to_rel, after=False):
200         # FIXME: Use after
201         assert after != True
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
207             from_wc = to_wc
208         else:
209             (from_wc, _) = self._get_rel_wc(from_rel, write_lock=True)
210         from_id = self.inventory.path2id(from_rel)
211         try:
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)
214         finally:
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()
219
220     def path_to_file_id(self, revnum, current_revnum, path):
221         """Generate a bzr file id from a Subversion file name. 
222         
223         :param revnum: Revision number.
224         :param path: Absolute path.
225         :return: Tuple with file id and revision id.
226         """
227         assert isinstance(revnum, int) and revnum >= 0
228         assert isinstance(path, basestring)
229
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)
234         return entry
235
236     def read_working_inventory(self):
237         inv = Inventory()
238
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
248                 inv.add(file)
249             else:
250                 file = InventoryFile(id, os.path.basename(relpath), parent_id)
251                 file.revision = revid
252                 try:
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)
257                     inv.add(file)
258                 except IOError:
259                     # Ignore non-existing files
260                     pass
261
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))):
271                         yield os.path.join(
272                                 self.branch.get_branch_path().strip("/"), 
273                                 subrelpath)
274                 else:
275                     find_copies(subrelpath)
276             svn.wc.adm_close(wc)
277
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,
282                                       svn.wc.schedule_add,
283                                       svn.wc.schedule_replace)
284             if entry.schedule == svn.wc.schedule_normal:
285                 assert entry.revision >= 0
286                 # Keep old id
287                 return self.path_to_file_id(entry.cmt_rev, entry.revision, 
288                         relpath)
289             elif entry.schedule == svn.wc.schedule_delete:
290                 return (None, None)
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)
304
305         def add_dir_to_inv(relpath, wc, parent_id):
306             entries = svn.wc.entries_read(wc, False)
307             entry = entries[""]
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)
311             if id is None:
312                 mutter('no id for %r' % entry.url)
313                 return
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
316
317             # First handle directory itself
318             inv.add_path(relpath, 'directory', id, parent_id).revision = revid
319             if relpath == "":
320                 inv.revision_id = revid
321
322             for name in entries:
323                 if name == "":
324                     continue
325
326                 subrelpath = os.path.join(relpath, name)
327
328                 entry = entries[name]
329                 assert entry
330                 
331                 if entry.kind == svn.core.svn_node_dir:
332                     subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath), 
333                                              False, 0, None)
334                     try:
335                         add_dir_to_inv(subrelpath, subwc, id)
336                     finally:
337                         svn.wc.adm_close(subwc)
338                 else:
339                     (subid, subrevid) = find_ids(entry, rootwc)
340                     if subid:
341                         add_file_to_inv(subrelpath, subid, subrevid, id)
342                     else:
343                         mutter('no id for %r' % entry.url)
344
345         rootwc = self._get_wc() 
346         try:
347             add_dir_to_inv("", rootwc, None)
348         finally:
349             svn.wc.adm_close(rootwc)
350
351         self._set_inventory(inv, dirty=False)
352         return inv
353
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
358             self.base_revnum = 0
359             self.base_tree = RevisionTree(self, Inventory(), revid)
360             return
361
362         rev = self.branch.lookup_revision_id(revid)
363         self.base_revnum = rev
364         self.base_revid = revid
365         self.base_tree = SvnBasisTree(self)
366
367         # TODO: Implement more efficient version
368         newrev = self.branch.repository.get_revision(revid)
369         newrevtree = self.branch.repository.revision_tree(revid)
370
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)
376
377             svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc, 
378                           False, revnum, 
379                           svn.core.svn_time_to_cstring(newrev.timestamp), 
380                           newrev.committer, None, False)
381
382             if newrevtree.inventory[id].kind != 'directory':
383                 return
384
385             entries = svn.wc.entries_read(wc, True)
386             for entry in entries:
387                 if entry == "":
388                     continue
389
390                 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
391                 try:
392                     update_settings(subwc, os.path.join(path, entry))
393                 finally:
394                     svn.wc.adm_close(subwc)
395
396         # Set proper version for all files in the wc
397         wc = self._get_wc(write_lock=True)
398         try:
399             update_settings(wc, "")
400         finally:
401             svn.wc.adm_close(wc)
402         self.base_revid = revid
403
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
411         # FIXME: Use verbose
412         # FIXME: Use reporter
413         # FIXME: Use strict
414         if local:
415             raise LocalCommitsUnsupported()
416
417         if specific_files:
418             specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
419         else:
420             specific_files = [self.basedir.encode('utf8')]
421
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")
426         else:
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")
431
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)
435         else:
436             extra = ""
437         wc = self._get_wc(write_lock=True)
438         try:
439             svn.wc.prop_set(SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), 
440                              self._get_bzr_revids() + extra,
441                              self.basedir, wc)
442             svn.wc.prop_set(SVN_PROP_BZR_REVISION_INFO, 
443                              generate_revision_metadata(timestamp, 
444                                                         timezone, 
445                                                         committer,
446                                                         revprops),
447                              self.basedir, wc)
448         finally:
449             svn.wc.adm_close(wc)
450
451         try:
452             commit_info = svn.client.commit3(specific_files, True, False, 
453                                          self.client_ctx)
454         except:
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), 
463                 self.base_revnum, 
464                 SVN_PROP_BZR_REVISION_INFO, ""), 
465                 self.basedir, wc)
466             svn.wc.adm_close(wc)
467             raise
468
469         self.client_ctx.log_msg_baton2 = None
470
471         revid = self.branch.generate_revision_id(commit_info.revision)
472
473         self.base_revid = revid
474         self.base_revnum = commit_info.revision
475         self.base_tree = SvnBasisTree(self)
476
477         return revid
478
479     def smart_add(self, file_list, recurse=True, action=None, save=True):
480         assert isinstance(recurse, bool)
481         if action is None:
482             action = bzrlib.add.AddAction()
483         # TODO: use action
484         if not file_list:
485             # no paths supplied: add the entire tree.
486             file_list = [u'.']
487         ignored = {}
488         added = []
489
490         for file_path in file_list:
491             todo = []
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)
495             try:
496                 if not self.inventory.has_filename(f):
497                     if save:
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):
505                             continue
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)
510                         todo.append(c_path)
511             finally:
512                 svn.wc.adm_close(wc)
513             if todo != []:
514                 cadded, cignored = self.smart_add(todo, recurse, action, save)
515                 added.extend(cadded)
516                 ignored.update(cignored)
517         return added, ignored
518
519     def add(self, files, ids=None, kinds=None):
520         # TODO: Use kinds
521         if isinstance(files, str):
522             files = [files]
523             if isinstance(ids, str):
524                 ids = [ids]
525         if ids is not None:
526             ids = iter(ids)
527         assert isinstance(files, list)
528         for f in files:
529             wc = self._get_wc(os.path.dirname(f), write_lock=True)
530             try:
531                 try:
532                     svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0, 
533                             None, None, None)
534                     if ids is not None:
535                         self._change_fileid_mapping(ids.next(), f, wc)
536                 except SubversionException, (_, num):
537                     if num == svn.core.SVN_ERR_ENTRY_EXISTS:
538                         continue
539                     elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
540                         raise NoSuchFile(path=f)
541                     raise
542             finally:
543                 svn.wc.adm_close(wc)
544         self.read_working_inventory()
545
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)
549
550         return self.base_tree
551
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)
570         return result
571
572     def get_file_sha1(self, file_id, path=None, stat_value=None):
573         if not path:
574             path = self._inventory.id2path(file_id)
575         return fingerprint_file(open(self.abspath(path)))['sha1']
576
577     def _change_fileid_mapping(self, id, path, wc=None):
578         if wc is None:
579             subwc = self._get_wc(write_lock=True)
580         else:
581             subwc = wc
582         new_entries = self._get_new_file_ids(subwc)
583         if id is None:
584             if new_entries.has_key(path):
585                 del new_entries[path]
586         else:
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()))
590         if existing != "":
591             svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, existing.encode("utf-8"), self.basedir, subwc)
592         if wc is None:
593             svn.wc.adm_close(subwc)
594
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:
601             return {}
602         return dict(map(lambda x: str(x).split("\t"), 
603             existing.splitlines()))
604
605     def _get_bzr_revids(self):
606         return self.branch.repository.branchprop_list.get_property(
607                 self.branch.get_branch_path(self.base_revnum), 
608                 self.base_revnum, 
609                 SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), "")
610
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), "")
615
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, "")
620
621     def set_pending_merges(self, merges):
622         """See MutableTree.set_pending_merges()."""
623         wc = self._get_wc(write_lock=True)
624         try:
625             # Set bzr:merge
626             if len(merges) > 0:
627                 bzr_merge = "\t".join(merges) + "\n"
628             else:
629                 bzr_merge = ""
630
631             svn.wc.prop_set(SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), 
632                                  self._get_bzr_merges() + bzr_merge, 
633                                  self.basedir, wc)
634
635             # Set svk:merge
636             svk_merge = ""
637             for merge in merges:
638                 try:
639                     svk_merge += revision_id_to_svk_feature(merge) + "\n"
640                 except InvalidRevisionId:
641                     pass
642
643             svn.wc.prop_set2(SVN_PROP_SVK_MERGE, 
644                              self._get_svk_merges() + svk_merge, self.basedir, 
645                              wc, False)
646         finally:
647             svn.wc.adm_close(wc)
648
649     def add_pending_merge(self, revid):
650         merges = self.pending_merges()
651         merges.append(revid)
652         self.set_pending_merges(merges)
653
654     def pending_merges(self):
655         merged = self._get_bzr_merges().splitlines()
656         wc = self._get_wc()
657         try:
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:
661                 set_merged = []
662             else:
663                 set_merged = merged_data.splitlines()
664         finally:
665             svn.wc.adm_close(wc)
666
667         assert (len(merged) == len(set_merged) or 
668                len(merged)+1 == len(set_merged))
669
670         if len(set_merged) > len(merged):
671             return set_merged[-1].split("\t")
672
673         return []
674
675     def _reset_data(self):
676         pass
677
678     def unlock(self):
679         # reverse order of locking.
680         try:
681             return self._control_files.unlock()
682         finally:
683             self.branch.unlock()
684
685
686
687 class SvnWorkingTreeFormat(WorkingTreeFormat):
688     """Subversion working copy format."""
689     def __get_matchingbzrdir(self):
690         return SvnWorkingTreeDirFormat()
691
692     _matchingbzrdir = property(__get_matchingbzrdir)
693
694     def get_format_description(self):
695         return "Subversion Working Copy"
696
697     def get_format_string(self):
698         return "Subversion Working Copy Format"
699
700     def initialize(self, a_bzrdir, revision_id=None):
701         raise NotImplementedError(self.initialize)
702
703     def open(self, a_bzrdir):
704         raise NotImplementedError(self.initialize)
705
706
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(".")
713         
714         # Open related remote repository + branch
715         wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
716         try:
717             svn_url = svn.wc.entry(self.local_path, wc, True).url
718         finally:
719             svn.wc.adm_close(wc)
720
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
725         
726     def clone(self, path, revision_id=None, force_new_repo=False):
727         raise NotImplementedError(self.clone)
728
729     def open_workingtree(self, _unsupported=False, recommend_upgrade=False):
730         return SvnWorkingTree(self, self.local_path, self.open_branch())
731
732     def sprout(self, url, revision_id=None, force_new_repo=False, 
733                recurse='down', possible_transports=None):
734         # FIXME: honor force_new_repo
735         # FIXME: Use recurse
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()
742         return result
743
744     def open_repository(self):
745         raise NoRepositoryPresent(self)
746
747     def find_repository(self):
748         return SvnRepository(self, self.svn_root_transport, self.remote_bzrdir.branch_path)
749
750     def needs_format_conversion(self, format=None):
751         if format is None:
752             format = BzrDirFormat.get_default_format()
753         return not isinstance(self._format, format.__class__)
754
755     def create_workingtree(self, revision_id=None):
756         """See BzrDir.create_workingtree().
757
758         Not implemented for Subversion because having a .svn directory
759         implies having a working copy.
760         """
761         raise NotImplementedError(self.create_workingtree)
762
763     def create_branch(self):
764         """See BzrDir.create_branch()."""
765         raise NotImplementedError(self.create_branch)
766
767     def open_branch(self, unsupported=True):
768         """See BzrDir.open_branch()."""
769         repos = self.find_repository()
770
771         try:
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)
777             raise
778
779         branch.bzrdir = self.remote_bzrdir
780  
781         return branch
782
783
784