1 # Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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
17 from binascii import hexlify
18 from bzrlib.bzrdir import BzrDirFormat, BzrDir
19 from bzrlib.errors import (InvalidRevisionId, NotBranchError, NoSuchFile,
20 NoRepositoryPresent, BzrError)
21 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile,
22 InventoryLink, ROOT_ID)
23 from bzrlib.lockable_files import TransportLock, LockableFiles
24 from bzrlib.lockdir import LockDir
25 from bzrlib.osutils import rand_bytes, fingerprint_file
26 from bzrlib.progress import DummyProgress
27 from bzrlib.revision import NULL_REVISION
28 from bzrlib.trace import mutter
29 from bzrlib.transport.local import LocalTransport
30 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
32 from branch import SvnBranch
33 from repository import (SvnRepository, escape_svn_path, SVN_PROP_BZR_MERGE,
34 SVN_PROP_SVK_MERGE, SVN_PROP_BZR_FILEIDS,
35 revision_id_to_svk_feature)
36 from scheme import BranchingScheme
37 from transport import (SvnRaTransport, svn_config, bzr_to_svn_url,
39 from tree import SvnBasisTree
45 import svn.core, svn.wc
46 from svn.core import SubversionException, Pool
48 class WorkingTreeInconsistent(BzrError):
49 _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
51 def __init__(self, min_revnum, max_revnum):
52 self.min_revnum = min_revnum
53 self.max_revnum = max_revnum
56 class SvnWorkingTree(WorkingTree):
57 """Implementation of WorkingTree that uses a Subversion
58 Working Copy for storage."""
59 def __init__(self, bzrdir, local_path, branch):
60 self._format = SvnWorkingTreeFormat()
61 self.basedir = local_path
66 self.client_ctx = svn.client.create_context()
67 self.client_ctx.config = svn_config
68 self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
69 self.client_ctx.auth_baton = _create_auth_baton(self.pool)
72 status = svn.wc.revision_status(self.basedir, None, True, None, None)
73 if status.min_rev != status.max_rev:
74 #raise WorkingTreeInconsistent(status.min_rev, status.max_rev)
75 rev = svn.core.svn_opt_revision_t()
76 rev.kind = svn.core.svn_opt_revision_number
77 rev.value.number = status.max_rev
78 assert status.max_rev == svn.client.update(self.basedir, rev,
79 True, self.client_ctx, Pool())
81 self.base_revnum = status.max_rev
82 self.base_tree = SvnBasisTree(self)
83 self.base_revid = branch.repository.generate_revision_id(
84 self.base_revnum, branch.branch_path)
86 self.read_working_inventory()
88 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
90 os.makedirs(self.controldir)
91 os.makedirs(os.path.join(self.controldir, 'lock'))
94 control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
95 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
106 def get_ignore_list(self):
107 ignores = [svn.wc.get_adm_dir()] + svn.wc.get_default_ignores(svn_config)
109 def dir_add(wc, prefix):
110 ignorestr = svn.wc.prop_get(svn.core.SVN_PROP_IGNORE, self.abspath(prefix).rstrip("/"), wc)
111 if ignorestr is not None:
112 for pat in ignorestr.splitlines():
113 ignores.append("./"+os.path.join(prefix, pat))
115 entries = svn.wc.entries_read(wc, False)
116 for entry in entries:
120 if entries[entry].kind != svn.core.svn_node_dir:
123 subprefix = os.path.join(prefix, entry)
125 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
127 dir_add(subwc, subprefix)
129 svn.wc.adm_close(subwc)
139 def _write_inventory(self, inv):
142 def is_control_filename(self, path):
143 return svn.wc.is_adm_dir(path)
145 def remove(self, files, verbose=False, to_file=None):
146 assert isinstance(files, list)
147 wc = self._get_wc(write_lock=True)
150 svn.wc.delete2(self.abspath(file), wc, None, None, None)
155 self._change_fileid_mapping(None, file)
156 self.read_working_inventory()
158 def _get_wc(self, relpath="", write_lock=False):
159 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"),
162 def _get_rel_wc(self, relpath, write_lock=False):
163 dir = os.path.dirname(relpath)
164 file = os.path.basename(relpath)
165 return (self._get_wc(dir, write_lock), file)
167 def move(self, from_paths, to_name):
168 revt = svn.core.svn_opt_revision_t()
169 revt.kind = svn.core.svn_opt_revision_working
170 for entry in from_paths:
172 to_wc = self._get_wc(to_name, write_lock=True)
173 svn.wc.copy(self.abspath(entry), to_wc,
174 os.path.basename(entry), None, None)
176 svn.wc.adm_close(to_wc)
178 from_wc = self._get_wc(write_lock=True)
179 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
181 svn.wc.adm_close(from_wc)
182 new_name = "%s/%s" % (to_name, os.path.basename(entry))
183 self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
184 self._change_fileid_mapping(None, entry)
186 self.read_working_inventory()
188 def rename_one(self, from_rel, to_rel):
189 revt = svn.core.svn_opt_revision_t()
190 revt.kind = svn.core.svn_opt_revision_unspecified
191 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
192 from_id = self.inventory.path2id(from_rel)
194 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
195 svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
197 svn.wc.adm_close(to_wc)
198 self._change_fileid_mapping(None, from_rel)
199 self._change_fileid_mapping(from_id, to_rel)
200 self.read_working_inventory()
202 def path_to_file_id(self, revnum, current_revnum, path):
203 """Generate a bzr file id from a Subversion file name.
205 :param revnum: Revision number.
206 :param path: Absolute path.
207 :return: Tuple with file id and revision id.
209 assert isinstance(revnum, int) and revnum >= 0
210 assert isinstance(path, basestring)
212 (bp, rp) = self.branch.repository.scheme.unprefix(path)
213 entry = self.base_tree.id_map[rp]
214 assert entry[0] is not None
217 def read_working_inventory(self):
220 def add_file_to_inv(relpath, id, revid, parent_id):
221 """Add a file to the inventory."""
222 if os.path.islink(self.abspath(relpath)):
223 file = InventoryLink(id, os.path.basename(relpath), parent_id)
224 file.revision = revid
225 file.symlink_target = os.readlink(self.abspath(relpath))
226 file.text_sha1 = None
227 file.text_size = None
228 file.executable = False
231 file = InventoryFile(id, os.path.basename(relpath), parent_id)
232 file.revision = revid
234 data = fingerprint_file(open(self.abspath(relpath)))
235 file.text_sha1 = data['sha1']
236 file.text_size = data['size']
237 file.executable = self.is_executable(id, relpath)
240 # Ignore non-existing files
243 def find_copies(url, relpath=""):
244 wc = self._get_wc(relpath)
245 entries = svn.wc.entries_read(wc, False)
246 for entry in entries.values():
247 subrelpath = os.path.join(relpath, entry.name)
248 if entry.name == "" or entry.kind != 'directory':
249 if ((entry.copyfrom_url == url or entry.url == url) and
250 not (entry.schedule in (svn.wc.schedule_delete,
251 svn.wc.schedule_replace))):
253 self.branch.branch_path.strip("/"),
256 find_copies(subrelpath)
259 def find_ids(entry, rootwc):
260 relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
261 assert entry.schedule in (svn.wc.schedule_normal,
262 svn.wc.schedule_delete,
264 svn.wc.schedule_replace)
265 if entry.schedule == svn.wc.schedule_normal:
266 assert entry.revision >= 0
268 return self.path_to_file_id(entry.cmt_rev, entry.revision,
270 elif entry.schedule == svn.wc.schedule_delete:
272 elif (entry.schedule == svn.wc.schedule_add or
273 entry.schedule == svn.wc.schedule_replace):
274 # See if the file this file was copied from disappeared
275 # and has no other copies -> in that case, take id of other file
276 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
277 return self.path_to_file_id(entry.copyfrom_rev, entry.revision,
278 entry.copyfrom_url[len(entry.repos):])
279 ids = self._get_new_file_ids(rootwc)
280 if ids.has_key(relpath):
281 return (ids[relpath], None)
282 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
284 def add_dir_to_inv(relpath, wc, parent_id):
285 entries = svn.wc.entries_read(wc, False)
287 (id, revid) = find_ids(entry, rootwc)
289 mutter('no id for %r' % entry.url)
292 # First handle directory itself
294 inv.add_path("", 'directory', ROOT_ID)
295 inv.revision_id = revid
297 inventry = InventoryDirectory(id, os.path.basename(relpath), parent_id)
298 inventry.revision = revid
305 subrelpath = os.path.join(relpath, name)
307 entry = entries[name]
310 if entry.kind == svn.core.svn_node_dir:
311 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
314 add_dir_to_inv(subrelpath, subwc, id)
316 svn.wc.adm_close(subwc)
318 (subid, subrevid) = find_ids(entry, rootwc)
320 add_file_to_inv(subrelpath, subid, subrevid, id)
322 mutter('no id for %r' % entry.url)
324 rootwc = self._get_wc()
326 add_dir_to_inv("", rootwc, None)
328 svn.wc.adm_close(rootwc)
330 self._set_inventory(inv, dirty=False)
333 def set_last_revision(self, revid):
334 mutter('setting last revision to %r' % revid)
335 if revid is None or revid == NULL_REVISION:
336 self.base_revid = revid
338 self.base_tree = RevisionTree(self, Inventory(), revid)
341 (bp, rev) = self.branch.repository.parse_revision_id(revid)
342 assert bp == self.branch.branch_path
343 self.base_revnum = rev
344 self.base_revid = revid
345 self.base_tree = SvnBasisTree(self)
347 # TODO: Implement more efficient version
348 newrev = self.branch.repository.get_revision(revid)
349 newrevtree = self.branch.repository.revision_tree(revid)
351 def update_settings(wc, path):
352 id = newrevtree.inventory.path2id(path)
353 mutter("Updating settings for %r" % id)
354 (_, revnum) = self.branch.repository.parse_revision_id(
355 newrevtree.inventory[id].revision)
357 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
359 svn.core.svn_time_to_cstring(newrev.timestamp),
360 newrev.committer, None, False)
362 if newrevtree.inventory[id].kind != 'directory':
365 entries = svn.wc.entries_read(wc, True)
366 for entry in entries:
370 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
372 update_settings(subwc, os.path.join(path, entry))
374 svn.wc.adm_close(subwc)
376 # Set proper version for all files in the wc
377 wc = self._get_wc(write_lock=True)
379 update_settings(wc, "")
382 self.base_revid = revid
384 def commit(self, message=None, message_callback=None, revprops=None, timestamp=None, timezone=None, committer=None,
385 rev_id=None, allow_pointless=True, strict=False, verbose=False, local=False, reporter=None, config=None,
386 specific_files=None):
387 assert timestamp is None
388 assert timezone is None
389 assert rev_id is None
392 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
394 specific_files = [self.basedir.encode('utf8')]
396 if message_callback is not None:
397 def log_message_func(items, pool):
398 """ Simple log message provider for unit tests. """
399 return str(message_callback(self))
401 assert isinstance(message, basestring)
402 def log_message_func(items, pool):
403 """ Simple log message provider for unit tests. """
406 self.client_ctx.log_msg_baton2 = log_message_func
407 commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
408 self.client_ctx.log_msg_baton2 = None
410 revid = self.branch.repository.generate_revision_id(
411 commit_info.revision, self.branch.branch_path)
413 self.base_revid = revid
414 self.base_revnum = commit_info.revision
415 self.base_tree = SvnBasisTree(self)
417 #FIXME: Use public API:
418 self.branch.revision_history()
419 self.branch._revision_history.append(revid)
423 def add(self, files, ids=None):
429 assert isinstance(files, list)
432 wc = self._get_wc(os.path.dirname(f), write_lock=True)
434 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
437 self._change_fileid_mapping(ids.pop(), f, wc)
438 except SubversionException, (_, num):
439 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
441 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
442 raise NoSuchFile(path=f)
446 self.read_working_inventory()
448 def basis_tree(self):
449 if self.base_revid is None or self.base_revid == NULL_REVISION:
450 return self.branch.repository.revision_tree(self.base_revid)
452 return self.base_tree
454 def pull(self, source, overwrite=False, stop_revision=None):
455 if stop_revision is None:
456 stop_revision = self.branch.last_revision()
457 rev = svn.core.svn_opt_revision_t()
458 rev.kind = svn.core.svn_opt_revision_number
459 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
460 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
461 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
462 return fetched-rev.value.number
464 def get_file_sha1(self, file_id, path=None, stat_value=None):
466 path = self._inventory.id2path(file_id)
467 return fingerprint_file(open(self.abspath(path)))['sha1']
469 def _change_fileid_mapping(self, id, path, wc=None):
471 subwc = self._get_wc(write_lock=True)
474 new_entries = self._get_new_file_ids(subwc)
476 if new_entries.has_key(path):
477 del new_entries[path]
479 new_entries[path] = id
480 committed = self.branch.repository.branchprop_list.get_property(
481 self.branch.branch_path,
483 SVN_PROP_BZR_FILEIDS, "")
484 existing = committed + "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
486 svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, str(existing), self.basedir, subwc)
488 svn.wc.adm_close(subwc)
490 def _get_new_file_ids(self, wc):
491 committed = self.branch.repository.branchprop_list.get_property(
492 self.branch.branch_path,
494 SVN_PROP_BZR_FILEIDS, "")
495 existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
499 return dict(map(lambda x: x.split("\t"), existing[len(committed):].splitlines()))
501 def _get_bzr_merges(self):
502 return self.branch.repository.branchprop_list.get_property(
503 self.branch.branch_path,
505 SVN_PROP_BZR_MERGE, "")
507 def _get_svk_merges(self):
508 return self.branch.repository.branchprop_list.get_property(
509 self.branch.branch_path,
511 SVN_PROP_SVK_MERGE, "")
513 def set_pending_merges(self, merges):
514 wc = self._get_wc(write_lock=True)
518 bzr_merge = "\t".join(merges) + "\n"
522 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
523 self._get_bzr_merges() + bzr_merge,
530 svk_merge += revision_id_to_svk_feature(merge) + "\n"
531 except InvalidRevisionId:
534 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
535 self._get_svk_merges() + svk_merge, self.basedir,
540 def add_pending_merge(self, revid):
541 merges = self.pending_merges()
543 self.set_pending_merges(existing)
545 def pending_merges(self):
546 merged = self._get_bzr_merges().splitlines()
549 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
550 if merged_data is None:
553 set_merged = merged_data.splitlines()
557 assert (len(merged) == len(set_merged) or
558 len(merged)+1 == len(set_merged))
560 if len(set_merged) > len(merged):
561 return set_merged[-1].split("\t")
566 class SvnWorkingTreeFormat(WorkingTreeFormat):
567 def get_format_description(self):
568 return "Subversion Working Copy"
570 def initialize(self, a_bzrdir, revision_id=None):
571 raise NotImplementedError(self.initialize)
573 def open(self, a_bzrdir):
574 raise NotImplementedError(self.initialize)
577 class SvnCheckout(BzrDir):
578 """BzrDir implementation for Subversion checkouts (directories
579 containing a .svn subdirectory."""
580 def __init__(self, transport, format):
581 super(SvnCheckout, self).__init__(transport, format)
582 self.local_path = transport.local_abspath(".")
584 # Open related remote repository + branch
585 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
587 svn_url = svn.wc.entry(self.local_path, wc, True).url
591 self.remote_transport = SvnRaTransport(svn_url)
592 self.svn_root_transport = SvnRaTransport(self.remote_transport.get_repos_root())
593 self.root_transport = self.transport = transport
595 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
596 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
597 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
598 if not self.scheme.is_branch(self.branch_path):
599 raise NotBranchError(path=self.transport.base)
601 def clone(self, path):
602 raise NotImplementedError(self.clone)
604 def open_workingtree(self, _unsupported=False):
605 return SvnWorkingTree(self, self.local_path, self.open_branch())
607 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
608 # FIXME: honor force_new_repo
609 result = BzrDirFormat.get_default_format().initialize(url)
610 repo = self.find_repository()
611 result_repo = repo.clone(result, revision_id, basis)
612 branch = self.open_branch()
613 branch.sprout(result, revision_id)
614 result.create_workingtree()
617 def open_repository(self):
618 raise NoRepositoryPresent(self)
620 def find_repository(self):
621 return SvnRepository(self, self.svn_root_transport)
623 def create_workingtree(self, revision_id=None):
624 """See BzrDir.create_workingtree().
626 Not implemented for Subversion because having a .svn directory
627 implies having a working copy.
629 raise NotImplementedError(self.create_workingtree)
631 def create_branch(self):
632 """See BzrDir.create_branch()."""
633 raise NotImplementedError(self.create_branch)
635 def open_branch(self, unsupported=True):
636 """See BzrDir.open_branch()."""
637 repos = self.find_repository()
640 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
641 except SubversionException, (msg, num):
642 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
643 raise NotBranchError(path=self.url)
650 class SvnWorkingTreeDirFormat(BzrDirFormat):
651 """Working Tree implementation that uses Subversion working copies."""
652 _lock_class = TransportLock
655 def probe_transport(klass, transport):
658 if isinstance(transport, LocalTransport) and \
659 transport.has(svn.wc.get_adm_dir()):
662 raise NotBranchError(path=transport.base)
664 def _open(self, transport):
665 return SvnCheckout(transport, self)
667 def get_format_string(self):
668 return 'Subversion Local Checkout'
670 def get_format_description(self):
671 return 'Subversion Local Checkout'
673 def initialize_on_transport(self, transport):
674 raise NotImplementedError(self.initialize_on_transport)