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