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