Merge 0.4.
[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                            OutOfDateTree)
25 from bzrlib.inventory import Inventory, InventoryFile, InventoryLink
26 from bzrlib.lockable_files import TransportLock, LockableFiles
27 from bzrlib.lockdir import LockDir
28 from bzrlib.osutils import file_kind, fingerprint_file
29 from bzrlib.revision import NULL_REVISION
30 from bzrlib.trace import mutter
31 from bzrlib.tree import RevisionTree
32 from bzrlib.transport.local import LocalTransport
33 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
34
35 from branch import SvnBranch
36 from convert import SvnConverter
37 from errors import LocalCommitsUnsupported, NoSvnRepositoryPresent
38 from mapping import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS, 
39                      SVN_PROP_BZR_REVISION_ID, SVN_PROP_BZR_REVISION_INFO)
40 from remote import SvnRemoteAccess
41 from repository import (SvnRepository, revision_id_to_svk_feature, 
42                         generate_revision_metadata, SVN_PROP_SVK_MERGE) 
43 from mapping import escape_svn_path
44 from scheme import BranchingScheme
45 from transport import (SvnRaTransport, bzr_to_svn_url, create_svn_client,
46                        svn_config) 
47 from tree import SvnBasisTree
48
49 import os
50 import urllib
51
52 import svn.core, svn.wc
53 from svn.core import SubversionException, Pool
54
55 from errors import NoCheckoutSupport
56 from format import get_rich_root_format
57
58 class WorkingTreeInconsistent(BzrError):
59     _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
60
61     def __init__(self, min_revnum, max_revnum):
62         self.min_revnum = min_revnum
63         self.max_revnum = max_revnum
64
65
66 class SvnWorkingTree(WorkingTree):
67     """WorkingTree implementation that uses a Subversion Working Copy for storage."""
68     def __init__(self, bzrdir, local_path, branch):
69         self._format = SvnWorkingTreeFormat()
70         self.basedir = local_path
71         self.bzrdir = bzrdir
72         self._branch = branch
73         self.base_revnum = 0
74         self.pool = Pool()
75         self.client_ctx = create_svn_client(self.pool)
76         self.client_ctx.log_msg_func2 = \
77                 svn.client.svn_swig_py_get_commit_log_func
78
79         self._get_wc()
80         status = svn.wc.revision_status(self.basedir, None, True, None, None)
81         if status.min_rev != status.max_rev:
82             #raise WorkingTreeInconsistent(status.min_rev, status.max_rev)
83             rev = svn.core.svn_opt_revision_t()
84             rev.kind = svn.core.svn_opt_revision_number
85             rev.value.number = status.max_rev
86             assert status.max_rev == svn.client.update(self.basedir, rev,
87                                      True, self.client_ctx, Pool())
88
89         self.base_revnum = status.max_rev
90         self.base_tree = SvnBasisTree(self)
91         self.base_revid = branch.generate_revision_id(self.base_revnum)
92
93         self.read_working_inventory()
94
95         self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 
96                                        'bzr')
97         try:
98             os.makedirs(self.controldir)
99             os.makedirs(os.path.join(self.controldir, 'lock'))
100         except OSError:
101             pass
102         control_transport = bzrdir.transport.clone(os.path.join(
103                                                    svn.wc.get_adm_dir(), 'bzr'))
104         self._control_files = LockableFiles(control_transport, 'lock', LockDir)
105
106     def get_ignore_list(self):
107         ignores = set([svn.wc.get_adm_dir()])
108         ignores.update(svn.wc.get_default_ignores(svn_config))
109
110         def dir_add(wc, prefix):
111             ignorestr = svn.wc.prop_get(svn.core.SVN_PROP_IGNORE, 
112                                         self.abspath(prefix).rstrip("/"), wc)
113             if ignorestr is not None:
114                 for pat in ignorestr.splitlines():
115                     ignores.add("./"+os.path.join(prefix, pat))
116
117             entries = svn.wc.entries_read(wc, False)
118             for entry in entries:
119                 if entry == "":
120                     continue
121
122                 if entries[entry].kind != svn.core.svn_node_dir:
123                     continue
124
125                 subprefix = os.path.join(prefix, entry)
126
127                 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 
128                                          0, None)
129                 try:
130                     dir_add(subwc, subprefix)
131                 finally:
132                     svn.wc.adm_close(subwc)
133
134         wc = self._get_wc()
135         try:
136             dir_add(wc, "")
137         finally:
138             svn.wc.adm_close(wc)
139
140         return ignores
141
142     def is_control_filename(self, path):
143         return svn.wc.is_adm_dir(path)
144
145     def apply_inventory_delta(self, changes):
146         raise NotImplementedError(self.apply_inventory_delta)
147
148     def update(self, change_reporter=None):
149         rev = svn.core.svn_opt_revision_t()
150         rev.kind = svn.core.svn_opt_revision_head
151         svn.client.update(self.basedir, rev, True, self.client_ctx)
152
153     def remove(self, files, verbose=False, to_file=None):
154         # FIXME: Use to_file argument
155         # FIXME: Use verbose argument
156         assert isinstance(files, list)
157         wc = self._get_wc(write_lock=True)
158         try:
159             for file in files:
160                 svn.wc.delete2(self.abspath(file), wc, None, None, None)
161         finally:
162             svn.wc.adm_close(wc)
163
164         for file in files:
165             self._change_fileid_mapping(None, file)
166         self.read_working_inventory()
167
168     def _get_wc(self, relpath="", write_lock=False):
169         return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), 
170                                 write_lock, 0, None)
171
172     def _get_rel_wc(self, relpath, write_lock=False):
173         dir = os.path.dirname(relpath)
174         file = os.path.basename(relpath)
175         return (self._get_wc(dir, write_lock), file)
176
177     def move(self, from_paths, to_dir=None, after=False, **kwargs):
178         # FIXME: Use after argument
179         assert after != True
180         revt = svn.core.svn_opt_revision_t()
181         revt.kind = svn.core.svn_opt_revision_working
182         for entry in from_paths:
183             try:
184                 to_wc = self._get_wc(to_dir, write_lock=True)
185                 svn.wc.copy(self.abspath(entry), to_wc, 
186                             os.path.basename(entry), None, None)
187             finally:
188                 svn.wc.adm_close(to_wc)
189             try:
190                 from_wc = self._get_wc(write_lock=True)
191                 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
192             finally:
193                 svn.wc.adm_close(from_wc)
194             new_name = urlutils.join(to_dir, os.path.basename(entry))
195             self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
196             self._change_fileid_mapping(None, entry)
197
198         self.read_working_inventory()
199
200     def rename_one(self, from_rel, to_rel, after=False):
201         # FIXME: Use after
202         assert after != True
203         revt = svn.core.svn_opt_revision_t()
204         revt.kind = svn.core.svn_opt_revision_unspecified
205         (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
206         if os.path.dirname(from_rel) == os.path.dirname(to_rel):
207             # Prevent lock contention
208             from_wc = to_wc
209         else:
210             (from_wc, _) = self._get_rel_wc(from_rel, write_lock=True)
211         from_id = self.inventory.path2id(from_rel)
212         try:
213             svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
214             svn.wc.delete2(self.abspath(from_rel), from_wc, None, None, None)
215         finally:
216             svn.wc.adm_close(to_wc)
217         self._change_fileid_mapping(None, from_rel)
218         self._change_fileid_mapping(from_id, to_rel)
219         self.read_working_inventory()
220
221     def path_to_file_id(self, revnum, current_revnum, path):
222         """Generate a bzr file id from a Subversion file name. 
223         
224         :param revnum: Revision number.
225         :param path: Absolute path within the Subversion repository.
226         :return: Tuple with file id and revision id.
227         """
228         assert isinstance(revnum, int) and revnum >= 0
229         assert isinstance(path, str)
230
231         rp = self.branch.unprefix(path)
232         entry = self.base_tree.id_map[rp]
233         assert entry[0] is not None
234         assert isinstance(entry[0], str), "fileid %r for %r is not a string" % (entry[0], path)
235         return entry
236
237     def read_working_inventory(self):
238         inv = Inventory()
239
240         def add_file_to_inv(relpath, id, revid, parent_id):
241             """Add a file to the inventory."""
242             assert isinstance(relpath, unicode)
243             if os.path.islink(self.abspath(relpath)):
244                 file = InventoryLink(id, os.path.basename(relpath), parent_id)
245                 file.revision = revid
246                 file.symlink_target = os.readlink(self.abspath(relpath))
247                 file.text_sha1 = None
248                 file.text_size = None
249                 file.executable = False
250                 inv.add(file)
251             else:
252                 file = InventoryFile(id, os.path.basename(relpath), parent_id)
253                 file.revision = revid
254                 try:
255                     data = fingerprint_file(open(self.abspath(relpath)))
256                     file.text_sha1 = data['sha1']
257                     file.text_size = data['size']
258                     file.executable = self.is_executable(id, relpath)
259                     inv.add(file)
260                 except IOError:
261                     # Ignore non-existing files
262                     pass
263
264         def find_copies(url, relpath=""):
265             wc = self._get_wc(relpath)
266             entries = svn.wc.entries_read(wc, False)
267             for entry in entries.values():
268                 subrelpath = os.path.join(relpath, entry.name)
269                 if entry.name == "" or entry.kind != 'directory':
270                     if ((entry.copyfrom_url == url or entry.url == url) and 
271                         not (entry.schedule in (svn.wc.schedule_delete,
272                                                 svn.wc.schedule_replace))):
273                         yield os.path.join(
274                                 self.branch.get_branch_path().strip("/"), 
275                                 subrelpath)
276                 else:
277                     find_copies(subrelpath)
278             svn.wc.adm_close(wc)
279
280         def find_ids(entry, rootwc):
281             relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
282             assert entry.schedule in (svn.wc.schedule_normal, 
283                                       svn.wc.schedule_delete,
284                                       svn.wc.schedule_add,
285                                       svn.wc.schedule_replace)
286             if entry.schedule == svn.wc.schedule_normal:
287                 assert entry.revision >= 0
288                 # Keep old id
289                 return self.path_to_file_id(entry.cmt_rev, entry.revision, 
290                         relpath)
291             elif entry.schedule == svn.wc.schedule_delete:
292                 return (None, None)
293             elif (entry.schedule == svn.wc.schedule_add or 
294                   entry.schedule == svn.wc.schedule_replace):
295                 # See if the file this file was copied from disappeared
296                 # and has no other copies -> in that case, take id of other file
297                 if (entry.copyfrom_url and 
298                     list(find_copies(entry.copyfrom_url)) == [relpath]):
299                     return self.path_to_file_id(entry.copyfrom_rev, 
300                         entry.revision, entry.copyfrom_url[len(entry.repos):])
301                 ids = self._get_new_file_ids(rootwc)
302                 if ids.has_key(relpath):
303                     return (ids[relpath], None)
304                 # FIXME: Generate more random file ids
305                 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
306
307         def add_dir_to_inv(relpath, wc, parent_id):
308             assert isinstance(relpath, unicode)
309             entries = svn.wc.entries_read(wc, False)
310             entry = entries[""]
311             assert parent_id is None or isinstance(parent_id, str), \
312                     "%r is not a string" % parent_id
313             (id, revid) = find_ids(entry, rootwc)
314             if id is None:
315                 mutter('no id for %r' % entry.url)
316                 return
317             assert revid is None or isinstance(revid, str), "%r is not a string" % revid
318             assert isinstance(id, str), "%r is not a string" % id
319
320             # First handle directory itself
321             inv.add_path(relpath.decode("utf-8"), 'directory', id, parent_id).revision = revid
322             if relpath == "":
323                 inv.revision_id = revid
324
325             for name in entries:
326                 if name == "":
327                     continue
328
329                 subrelpath = os.path.join(relpath, name.decode("utf-8"))
330
331                 entry = entries[name]
332                 assert entry
333                 
334                 if entry.kind == svn.core.svn_node_dir:
335                     subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath), 
336                                              False, 0, None)
337                     try:
338                         add_dir_to_inv(subrelpath, subwc, id)
339                     finally:
340                         svn.wc.adm_close(subwc)
341                 else:
342                     (subid, subrevid) = find_ids(entry, rootwc)
343                     if subid:
344                         add_file_to_inv(subrelpath, subid, subrevid, id)
345                     else:
346                         mutter('no id for %r' % entry.url)
347
348         rootwc = self._get_wc() 
349         try:
350             add_dir_to_inv(u"", rootwc, None)
351         finally:
352             svn.wc.adm_close(rootwc)
353
354         self._set_inventory(inv, dirty=False)
355         return inv
356
357     def set_last_revision(self, revid):
358         mutter('setting last revision to %r' % revid)
359         if revid is None or revid == NULL_REVISION:
360             self.base_revid = revid
361             self.base_revnum = 0
362             self.base_tree = RevisionTree(self, Inventory(), revid)
363             return
364
365         rev = self.branch.lookup_revision_id(revid)
366         self.base_revnum = rev
367         self.base_revid = revid
368         self.base_tree = SvnBasisTree(self)
369
370         # TODO: Implement more efficient version
371         newrev = self.branch.repository.get_revision(revid)
372         newrevtree = self.branch.repository.revision_tree(revid)
373
374         def update_settings(wc, path):
375             id = newrevtree.inventory.path2id(path)
376             mutter("Updating settings for %r" % id)
377             revnum = self.branch.lookup_revision_id(
378                     newrevtree.inventory[id].revision)
379
380             svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc, 
381                           False, revnum, 
382                           svn.core.svn_time_to_cstring(newrev.timestamp), 
383                           newrev.committer, None, False)
384
385             if newrevtree.inventory[id].kind != 'directory':
386                 return
387
388             entries = svn.wc.entries_read(wc, True)
389             for entry in entries:
390                 if entry == "":
391                     continue
392
393                 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
394                 try:
395                     update_settings(subwc, os.path.join(path, entry))
396                 finally:
397                     svn.wc.adm_close(subwc)
398
399         # Set proper version for all files in the wc
400         wc = self._get_wc(write_lock=True)
401         try:
402             update_settings(wc, "")
403         finally:
404             svn.wc.adm_close(wc)
405         self.base_revid = revid
406
407     def commit(self, message=None, message_callback=None, revprops=None, 
408                timestamp=None, timezone=None, committer=None, rev_id=None, 
409                allow_pointless=True, strict=False, verbose=False, local=False, 
410                reporter=None, config=None, specific_files=None, author=None):
411         if author is not None:
412             revprops['author'] = author
413         # FIXME: Use allow_pointless
414         # FIXME: Use verbose
415         # FIXME: Use reporter
416         # FIXME: Use strict
417         if local:
418             raise LocalCommitsUnsupported()
419
420         if specific_files:
421             specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
422         else:
423             specific_files = [self.basedir.encode('utf8')]
424
425         if message_callback is not None:
426             def log_message_func(items, pool):
427                 """ Simple log message provider for unit tests. """
428                 return message_callback(self).encode("utf-8")
429         else:
430             assert isinstance(message, basestring)
431             def log_message_func(items, pool):
432                 """ Simple log message provider for unit tests. """
433                 return message.encode("utf-8")
434
435         self.client_ctx.log_msg_baton2 = log_message_func
436         if rev_id is not None:
437             extra = "%d %s\n" % (self.branch.revno()+1, rev_id)
438         else:
439             extra = ""
440         wc = self._get_wc(write_lock=True)
441         try:
442             svn.wc.prop_set(SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), 
443                              self._get_bzr_revids() + extra,
444                              self.basedir, wc)
445             svn.wc.prop_set(SVN_PROP_BZR_REVISION_INFO, 
446                              generate_revision_metadata(timestamp, 
447                                                         timezone, 
448                                                         committer,
449                                                         revprops),
450                              self.basedir, wc)
451         finally:
452             svn.wc.adm_close(wc)
453
454         try:
455             try:
456                 commit_info = svn.client.commit3(specific_files, True, False, 
457                                                  self.client_ctx)
458             except SubversionException, (_, num):
459                 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
460                     raise OutOfDateTree(self)
461                 raise
462         except:
463             # Reset properties so the next subversion commit won't 
464             # accidently set these properties.
465             wc = self._get_wc(write_lock=True)
466             svn.wc.prop_set(SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), 
467                              self._get_bzr_revids(), self.basedir, wc)
468             svn.wc.prop_set(SVN_PROP_BZR_REVISION_INFO, 
469                 self.branch.repository.branchprop_list.get_property(
470                 self.branch.get_branch_path(self.base_revnum), 
471                 self.base_revnum, 
472                 SVN_PROP_BZR_REVISION_INFO, ""), 
473                 self.basedir, wc)
474             svn.wc.adm_close(wc)
475             raise
476
477         self.client_ctx.log_msg_baton2 = None
478
479         revid = self.branch.generate_revision_id(commit_info.revision)
480
481         self.base_revid = revid
482         self.base_revnum = commit_info.revision
483         self.base_tree = SvnBasisTree(self)
484
485         return revid
486
487     def smart_add(self, file_list, recurse=True, action=None, save=True):
488         assert isinstance(recurse, bool)
489         if action is None:
490             action = bzrlib.add.AddAction()
491         # TODO: use action
492         if not file_list:
493             # no paths supplied: add the entire tree.
494             file_list = [u'.']
495         ignored = {}
496         added = []
497
498         for file_path in file_list:
499             todo = []
500             file_path = os.path.abspath(file_path)
501             f = self.relpath(file_path)
502             wc = self._get_wc(os.path.dirname(f), write_lock=True)
503             try:
504                 if not self.inventory.has_filename(f):
505                     if save:
506                         mutter('adding %r' % file_path)
507                         svn.wc.add2(file_path, wc, None, 0, None, None, None)
508                     added.append(file_path)
509                 if recurse and file_kind(file_path) == 'directory':
510                     # Filter out ignored files and update ignored
511                     for c in os.listdir(file_path):
512                         if self.is_control_filename(c):
513                             continue
514                         c_path = os.path.join(file_path, c)
515                         ignore_glob = self.is_ignored(c)
516                         if ignore_glob is not None:
517                             ignored.setdefault(ignore_glob, []).append(c_path)
518                         todo.append(c_path)
519             finally:
520                 svn.wc.adm_close(wc)
521             if todo != []:
522                 cadded, cignored = self.smart_add(todo, recurse, action, save)
523                 added.extend(cadded)
524                 ignored.update(cignored)
525         return added, ignored
526
527     def add(self, files, ids=None, kinds=None):
528         # TODO: Use kinds
529         if isinstance(files, str):
530             files = [files]
531             if isinstance(ids, str):
532                 ids = [ids]
533         if ids is not None:
534             ids = iter(ids)
535         assert isinstance(files, list)
536         for f in files:
537             wc = self._get_wc(os.path.dirname(f), write_lock=True)
538             try:
539                 try:
540                     svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0, 
541                             None, None, None)
542                     if ids is not None:
543                         self._change_fileid_mapping(ids.next(), f, wc)
544                 except SubversionException, (_, num):
545                     if num == svn.core.SVN_ERR_ENTRY_EXISTS:
546                         continue
547                     elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
548                         raise NoSuchFile(path=f)
549                     raise
550             finally:
551                 svn.wc.adm_close(wc)
552         self.read_working_inventory()
553
554     def basis_tree(self):
555         if self.base_revid is None or self.base_revid == NULL_REVISION:
556             return self.branch.repository.revision_tree(self.base_revid)
557
558         return self.base_tree
559
560     def pull(self, source, overwrite=False, stop_revision=None, 
561              delta_reporter=None, possible_transports=None):
562         # FIXME: Use delta_reporter
563         # FIXME: Use overwrite
564         result = PullResult()
565         result.source_branch = source
566         result.master_branch = None
567         result.target_branch = self.branch
568         (result.old_revno, result.old_revid) = self.branch.last_revision_info()
569         if stop_revision is None:
570             stop_revision = self.branch.last_revision()
571         rev = svn.core.svn_opt_revision_t()
572         rev.kind = svn.core.svn_opt_revision_number
573         rev.value.number = self.branch.lookup_revision_id(stop_revision)
574         fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
575         self.base_revid = self.branch.generate_revision_id(fetched)
576         result.new_revid = self.base_revid
577         result.new_revno = self.branch.revision_id_to_revno(result.new_revid)
578         return result
579
580     def get_file_sha1(self, file_id, path=None, stat_value=None):
581         if not path:
582             path = self._inventory.id2path(file_id)
583         return fingerprint_file(open(self.abspath(path)))['sha1']
584
585     def _change_fileid_mapping(self, id, path, wc=None):
586         if wc is None:
587             subwc = self._get_wc(write_lock=True)
588         else:
589             subwc = wc
590         new_entries = self._get_new_file_ids(subwc)
591         if id is None:
592             if new_entries.has_key(path):
593                 del new_entries[path]
594         else:
595             assert isinstance(id, str)
596             new_entries[path] = id
597         existing = "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
598         if existing != "":
599             svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, existing.encode("utf-8"), self.basedir, subwc)
600         if wc is None:
601             svn.wc.adm_close(subwc)
602
603     def _get_new_file_ids(self, wc):
604         committed = self.branch.repository.branchprop_list.get_property(
605                 self.branch.get_branch_path(self.base_revnum), self.base_revnum, 
606                 SVN_PROP_BZR_FILEIDS, "")
607         existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
608         if existing is None or committed == existing:
609             return {}
610         return dict(map(lambda x: str(x).split("\t"), 
611             existing.splitlines()))
612
613     def _get_bzr_revids(self):
614         return self.branch.repository.branchprop_list.get_property(
615                 self.branch.get_branch_path(self.base_revnum), 
616                 self.base_revnum, 
617                 SVN_PROP_BZR_REVISION_ID+str(self.branch.scheme), "")
618
619     def _get_bzr_merges(self):
620         return self.branch.repository.branchprop_list.get_property(
621                 self.branch.get_branch_path(self.base_revnum), 
622                 self.base_revnum, SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), "")
623
624     def _get_svk_merges(self):
625         return self.branch.repository.branchprop_list.get_property(
626                 self.branch.get_branch_path(self.base_revnum), 
627                 self.base_revnum, SVN_PROP_SVK_MERGE, "")
628
629     def set_pending_merges(self, merges):
630         """See MutableTree.set_pending_merges()."""
631         wc = self._get_wc(write_lock=True)
632         try:
633             # Set bzr:merge
634             if len(merges) > 0:
635                 bzr_merge = "\t".join(merges) + "\n"
636             else:
637                 bzr_merge = ""
638
639             svn.wc.prop_set(SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), 
640                                  self._get_bzr_merges() + bzr_merge, 
641                                  self.basedir, wc)
642
643             # Set svk:merge
644             svk_merge = ""
645             for merge in merges:
646                 try:
647                     svk_merge += revision_id_to_svk_feature(merge) + "\n"
648                 except InvalidRevisionId:
649                     pass
650
651             svn.wc.prop_set2(SVN_PROP_SVK_MERGE, 
652                              self._get_svk_merges() + svk_merge, self.basedir, 
653                              wc, False)
654         finally:
655             svn.wc.adm_close(wc)
656
657     def add_pending_merge(self, revid):
658         merges = self.pending_merges()
659         merges.append(revid)
660         self.set_pending_merges(merges)
661
662     def pending_merges(self):
663         merged = self._get_bzr_merges().splitlines()
664         wc = self._get_wc()
665         try:
666             merged_data = svn.wc.prop_get(
667                 SVN_PROP_BZR_ANCESTRY+str(self.branch.scheme), self.basedir, wc)
668             if merged_data is None:
669                 set_merged = []
670             else:
671                 set_merged = merged_data.splitlines()
672         finally:
673             svn.wc.adm_close(wc)
674
675         assert (len(merged) == len(set_merged) or 
676                len(merged)+1 == len(set_merged))
677
678         if len(set_merged) > len(merged):
679             return set_merged[-1].split("\t")
680
681         return []
682
683     def _reset_data(self):
684         pass
685
686     def unlock(self):
687         # non-implementation specific cleanup
688         self._cleanup()
689
690         # reverse order of locking.
691         try:
692             return self._control_files.unlock()
693         finally:
694             self.branch.unlock()
695
696
697
698 class SvnWorkingTreeFormat(WorkingTreeFormat):
699     """Subversion working copy format."""
700     def __get_matchingbzrdir(self):
701         return SvnWorkingTreeDirFormat()
702
703     _matchingbzrdir = property(__get_matchingbzrdir)
704
705     def get_format_description(self):
706         return "Subversion Working Copy"
707
708     def get_format_string(self):
709         return "Subversion Working Copy Format"
710
711     def initialize(self, a_bzrdir, revision_id=None):
712         raise NotImplementedError(self.initialize)
713
714     def open(self, a_bzrdir):
715         raise NotImplementedError(self.initialize)
716
717
718 class SvnCheckout(BzrDir):
719     """BzrDir implementation for Subversion checkouts (directories 
720     containing a .svn subdirectory."""
721     def __init__(self, transport, format):
722         super(SvnCheckout, self).__init__(transport, format)
723         self.local_path = transport.local_abspath(".")
724         
725         # Open related remote repository + branch
726         wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
727         try:
728             svn_url = svn.wc.entry(self.local_path, wc, True).url
729         finally:
730             svn.wc.adm_close(wc)
731
732         remote_transport = SvnRaTransport(svn_url)
733         self.remote_bzrdir = SvnRemoteAccess(remote_transport)
734         self.svn_root_transport = remote_transport.clone_root()
735         self.root_transport = self.transport = transport
736         
737     def clone(self, path, revision_id=None, force_new_repo=False):
738         raise NotImplementedError(self.clone)
739
740     def open_workingtree(self, _unsupported=False, recommend_upgrade=False):
741         return SvnWorkingTree(self, self.local_path, self.open_branch())
742
743     def sprout(self, url, revision_id=None, force_new_repo=False, 
744                recurse='down', possible_transports=None, accelerator_tree=None):
745         # FIXME: honor force_new_repo
746         # FIXME: Use recurse
747         result = get_rich_root_format().initialize(url)
748         repo = self.find_repository()
749         repo.clone(result, revision_id)
750         branch = self.open_branch()
751         branch.sprout(result, revision_id)
752         result.create_workingtree()
753         return result
754
755     def open_repository(self):
756         raise NoRepositoryPresent(self)
757
758     def find_repository(self):
759         return SvnRepository(self, self.svn_root_transport, self.remote_bzrdir.branch_path)
760
761     def needs_format_conversion(self, format=None):
762         if format is None:
763             format = BzrDirFormat.get_default_format()
764         return not isinstance(self._format, format.__class__)
765
766     def create_workingtree(self, revision_id=None):
767         """See BzrDir.create_workingtree().
768
769         Not implemented for Subversion because having a .svn directory
770         implies having a working copy.
771         """
772         raise NotImplementedError(self.create_workingtree)
773
774     def create_branch(self):
775         """See BzrDir.create_branch()."""
776         raise NotImplementedError(self.create_branch)
777
778     def open_branch(self, unsupported=True):
779         """See BzrDir.open_branch()."""
780         repos = self.find_repository()
781
782         try:
783             branch = SvnBranch(self.svn_root_transport.base, repos, 
784                                self.remote_bzrdir.branch_path)
785         except SubversionException, (_, num):
786             if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
787                 raise NotBranchError(path=self.base)
788             raise
789
790         branch.bzrdir = self.remote_bzrdir
791  
792         return branch
793
794
795