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