1 # Copyright (C) 2005-2007 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.branch import PullResult
19 from bzrlib.bzrdir import BzrDirFormat, BzrDir
20 from bzrlib.errors import (InvalidRevisionId, NotBranchError, NoSuchFile,
21 NoRepositoryPresent, BzrError)
22 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile,
23 InventoryLink, ROOT_ID)
24 from bzrlib.lockable_files import TransportLock, LockableFiles
25 from bzrlib.lockdir import LockDir
26 from bzrlib.osutils import rand_bytes, fingerprint_file
27 from bzrlib.progress import DummyProgress
28 from bzrlib.revision import NULL_REVISION
29 from bzrlib.trace import mutter
30 from bzrlib.transport.local import LocalTransport
31 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
33 from branch import SvnBranch
34 from repository import (SvnRepository, escape_svn_path, SVN_PROP_BZR_MERGE,
35 SVN_PROP_SVK_MERGE, SVN_PROP_BZR_FILEIDS,
36 revision_id_to_svk_feature)
37 from scheme import BranchingScheme
38 from transport import (SvnRaTransport, svn_config, bzr_to_svn_url,
40 from tree import SvnBasisTree
46 import svn.core, svn.wc
47 from svn.core import SubversionException, Pool
49 class WorkingTreeInconsistent(BzrError):
50 _fmt = """Working copy is in inconsistent state (%(min_revnum)d:%(max_revnum)d)"""
52 def __init__(self, min_revnum, max_revnum):
53 self.min_revnum = min_revnum
54 self.max_revnum = max_revnum
57 class SvnWorkingTree(WorkingTree):
58 """Implementation of WorkingTree that uses a Subversion
59 Working Copy for storage."""
60 def __init__(self, bzrdir, local_path, branch):
61 self._format = SvnWorkingTreeFormat()
62 self.basedir = local_path
67 self.client_ctx = svn.client.create_context()
68 self.client_ctx.config = svn_config
69 self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
70 self.client_ctx.auth_baton = _create_auth_baton(self.pool)
73 status = svn.wc.revision_status(self.basedir, None, True, None, None)
74 if status.min_rev != status.max_rev:
75 #raise WorkingTreeInconsistent(status.min_rev, status.max_rev)
76 rev = svn.core.svn_opt_revision_t()
77 rev.kind = svn.core.svn_opt_revision_number
78 rev.value.number = status.max_rev
79 assert status.max_rev == svn.client.update(self.basedir, rev,
80 True, self.client_ctx, Pool())
82 self.base_revnum = status.max_rev
83 self.base_tree = SvnBasisTree(self)
84 self.base_revid = branch.repository.generate_revision_id(
85 self.base_revnum, branch.branch_path)
87 self.read_working_inventory()
89 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
91 os.makedirs(self.controldir)
92 os.makedirs(os.path.join(self.controldir, 'lock'))
95 control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
96 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
107 def get_ignore_list(self):
108 ignores = [svn.wc.get_adm_dir()] + svn.wc.get_default_ignores(svn_config)
110 def dir_add(wc, prefix):
111 ignorestr = svn.wc.prop_get(svn.core.SVN_PROP_IGNORE, self.abspath(prefix).rstrip("/"), wc)
112 if ignorestr is not None:
113 for pat in ignorestr.splitlines():
114 ignores.append("./"+os.path.join(prefix, pat))
116 entries = svn.wc.entries_read(wc, False)
117 for entry in entries:
121 if entries[entry].kind != svn.core.svn_node_dir:
124 subprefix = os.path.join(prefix, entry)
126 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
128 dir_add(subwc, subprefix)
130 svn.wc.adm_close(subwc)
140 def _write_inventory(self, inv):
143 def is_control_filename(self, path):
144 return svn.wc.is_adm_dir(path)
146 def remove(self, files, verbose=False, to_file=None):
147 assert isinstance(files, list)
148 wc = self._get_wc(write_lock=True)
151 svn.wc.delete2(self.abspath(file), wc, None, None, None)
156 self._change_fileid_mapping(None, file)
157 self.read_working_inventory()
159 def _get_wc(self, relpath="", write_lock=False):
160 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"),
163 def _get_rel_wc(self, relpath, write_lock=False):
164 dir = os.path.dirname(relpath)
165 file = os.path.basename(relpath)
166 return (self._get_wc(dir, write_lock), file)
168 def move(self, from_paths, to_name):
169 revt = svn.core.svn_opt_revision_t()
170 revt.kind = svn.core.svn_opt_revision_working
171 for entry in from_paths:
173 to_wc = self._get_wc(to_name, write_lock=True)
174 svn.wc.copy(self.abspath(entry), to_wc,
175 os.path.basename(entry), None, None)
177 svn.wc.adm_close(to_wc)
179 from_wc = self._get_wc(write_lock=True)
180 svn.wc.delete2(self.abspath(entry), from_wc, None, None, None)
182 svn.wc.adm_close(from_wc)
183 new_name = "%s/%s" % (to_name, os.path.basename(entry))
184 self._change_fileid_mapping(self.inventory.path2id(entry), new_name)
185 self._change_fileid_mapping(None, entry)
187 self.read_working_inventory()
189 def rename_one(self, from_rel, to_rel):
190 revt = svn.core.svn_opt_revision_t()
191 revt.kind = svn.core.svn_opt_revision_unspecified
192 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
193 from_id = self.inventory.path2id(from_rel)
195 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
196 svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
198 svn.wc.adm_close(to_wc)
199 self._change_fileid_mapping(None, from_rel)
200 self._change_fileid_mapping(from_id, to_rel)
201 self.read_working_inventory()
203 def path_to_file_id(self, revnum, current_revnum, path):
204 """Generate a bzr file id from a Subversion file name.
206 :param revnum: Revision number.
207 :param path: Absolute path.
208 :return: Tuple with file id and revision id.
210 assert isinstance(revnum, int) and revnum >= 0
211 assert isinstance(path, basestring)
213 (bp, rp) = self.branch.repository.scheme.unprefix(path)
214 entry = self.base_tree.id_map[rp]
215 assert entry[0] is not None
218 def read_working_inventory(self):
221 def add_file_to_inv(relpath, id, revid, parent_id):
222 """Add a file to the inventory."""
223 if os.path.islink(self.abspath(relpath)):
224 file = InventoryLink(id, os.path.basename(relpath), parent_id)
225 file.revision = revid
226 file.symlink_target = os.readlink(self.abspath(relpath))
227 file.text_sha1 = None
228 file.text_size = None
229 file.executable = False
232 file = InventoryFile(id, os.path.basename(relpath), parent_id)
233 file.revision = revid
235 data = fingerprint_file(open(self.abspath(relpath)))
236 file.text_sha1 = data['sha1']
237 file.text_size = data['size']
238 file.executable = self.is_executable(id, relpath)
241 # Ignore non-existing files
244 def find_copies(url, relpath=""):
245 wc = self._get_wc(relpath)
246 entries = svn.wc.entries_read(wc, False)
247 for entry in entries.values():
248 subrelpath = os.path.join(relpath, entry.name)
249 if entry.name == "" or entry.kind != 'directory':
250 if ((entry.copyfrom_url == url or entry.url == url) and
251 not (entry.schedule in (svn.wc.schedule_delete,
252 svn.wc.schedule_replace))):
254 self.branch.branch_path.strip("/"),
257 find_copies(subrelpath)
260 def find_ids(entry, rootwc):
261 relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
262 assert entry.schedule in (svn.wc.schedule_normal,
263 svn.wc.schedule_delete,
265 svn.wc.schedule_replace)
266 if entry.schedule == svn.wc.schedule_normal:
267 assert entry.revision >= 0
269 return self.path_to_file_id(entry.cmt_rev, entry.revision,
271 elif entry.schedule == svn.wc.schedule_delete:
273 elif (entry.schedule == svn.wc.schedule_add or
274 entry.schedule == svn.wc.schedule_replace):
275 # See if the file this file was copied from disappeared
276 # and has no other copies -> in that case, take id of other file
277 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
278 return self.path_to_file_id(entry.copyfrom_rev, entry.revision,
279 entry.copyfrom_url[len(entry.repos):])
280 ids = self._get_new_file_ids(rootwc)
281 if ids.has_key(relpath):
282 return (ids[relpath], None)
283 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
285 def add_dir_to_inv(relpath, wc, parent_id):
286 entries = svn.wc.entries_read(wc, False)
288 assert parent_id is None or isinstance(parent_id, str), "%r is not a string" % parent_id
289 (id, revid) = find_ids(entry, rootwc)
291 mutter('no id for %r' % entry.url)
293 assert isinstance(id, str), "%r is not a string" % id
295 # First handle directory itself
296 inv.add_path(relpath, 'directory', id, parent_id).revision = revid
302 subrelpath = os.path.join(relpath, name)
304 entry = entries[name]
307 if entry.kind == svn.core.svn_node_dir:
308 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
311 add_dir_to_inv(subrelpath, subwc, id)
313 svn.wc.adm_close(subwc)
315 (subid, subrevid) = find_ids(entry, rootwc)
317 add_file_to_inv(subrelpath, subid, subrevid, id)
319 mutter('no id for %r' % entry.url)
321 rootwc = self._get_wc()
323 add_dir_to_inv("", rootwc, None)
325 svn.wc.adm_close(rootwc)
327 self._set_inventory(inv, dirty=False)
330 def set_last_revision(self, revid):
331 mutter('setting last revision to %r' % revid)
332 if revid is None or revid == NULL_REVISION:
333 self.base_revid = revid
335 self.base_tree = RevisionTree(self, Inventory(), revid)
338 (bp, rev) = self.branch.repository.parse_revision_id(revid)
339 assert bp == self.branch.branch_path
340 self.base_revnum = rev
341 self.base_revid = revid
342 self.base_tree = SvnBasisTree(self)
344 # TODO: Implement more efficient version
345 newrev = self.branch.repository.get_revision(revid)
346 newrevtree = self.branch.repository.revision_tree(revid)
348 def update_settings(wc, path):
349 id = newrevtree.inventory.path2id(path)
350 mutter("Updating settings for %r" % id)
351 (_, revnum) = self.branch.repository.parse_revision_id(
352 newrevtree.inventory[id].revision)
354 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
356 svn.core.svn_time_to_cstring(newrev.timestamp),
357 newrev.committer, None, False)
359 if newrevtree.inventory[id].kind != 'directory':
362 entries = svn.wc.entries_read(wc, True)
363 for entry in entries:
367 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
369 update_settings(subwc, os.path.join(path, entry))
371 svn.wc.adm_close(subwc)
373 # Set proper version for all files in the wc
374 wc = self._get_wc(write_lock=True)
376 update_settings(wc, "")
379 self.base_revid = revid
381 def commit(self, message=None, message_callback=None, revprops=None, timestamp=None, timezone=None, committer=None,
382 rev_id=None, allow_pointless=True, strict=False, verbose=False, local=False, reporter=None, config=None,
383 specific_files=None):
384 assert timestamp is None
385 assert timezone is None
386 assert rev_id is None
389 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
391 specific_files = [self.basedir.encode('utf8')]
393 if message_callback is not None:
394 def log_message_func(items, pool):
395 """ Simple log message provider for unit tests. """
396 return message_callback(self).encode("utf-8")
398 assert isinstance(message, basestring)
399 def log_message_func(items, pool):
400 """ Simple log message provider for unit tests. """
401 return message.encode("utf-8")
403 self.client_ctx.log_msg_baton2 = log_message_func
404 commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
405 self.client_ctx.log_msg_baton2 = None
407 revid = self.branch.repository.generate_revision_id(
408 commit_info.revision, self.branch.branch_path)
410 self.base_revid = revid
411 self.base_revnum = commit_info.revision
412 self.base_tree = SvnBasisTree(self)
414 #FIXME: Use public API:
415 self.branch.revision_history()
416 self.branch._revision_history.append(revid)
420 def add(self, files, ids=None):
421 if isinstance(files, str):
423 if isinstance(ids, str):
428 assert isinstance(files, list)
431 wc = self._get_wc(os.path.dirname(f), write_lock=True)
433 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
436 self._change_fileid_mapping(ids.pop(), f, wc)
437 except SubversionException, (_, num):
438 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
440 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
441 raise NoSuchFile(path=f)
445 self.read_working_inventory()
447 def basis_tree(self):
448 if self.base_revid is None or self.base_revid == NULL_REVISION:
449 return self.branch.repository.revision_tree(self.base_revid)
451 return self.base_tree
453 def pull(self, source, overwrite=False, stop_revision=None, delta_reporter=None):
454 result = PullResult()
455 result.source_branch = source
456 result.master_branch = None
457 result.target_branch = self.branch
458 (result.old_revno, result.old_revid) = self.branch.last_revision_info()
459 if stop_revision is None:
460 stop_revision = self.branch.last_revision()
461 rev = svn.core.svn_opt_revision_t()
462 rev.kind = svn.core.svn_opt_revision_number
463 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
464 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
465 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
466 result.new_revid = self.branch.generate_revision_id(fetched)
467 result.new_revno = self.branch.revision_id_to_revno(result.new_revid)
470 def get_file_sha1(self, file_id, path=None, stat_value=None):
472 path = self._inventory.id2path(file_id)
473 return fingerprint_file(open(self.abspath(path)))['sha1']
475 def _change_fileid_mapping(self, id, path, wc=None):
477 subwc = self._get_wc(write_lock=True)
480 new_entries = self._get_new_file_ids(subwc)
482 if new_entries.has_key(path):
483 del new_entries[path]
485 new_entries[path] = id
486 committed = self.branch.repository.branchprop_list.get_property(
487 self.branch.branch_path,
489 SVN_PROP_BZR_FILEIDS, "")
490 existing = committed + "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
492 svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, existing.encode("utf-8"), self.basedir, subwc)
494 svn.wc.adm_close(subwc)
496 def _get_new_file_ids(self, wc):
497 committed = self.branch.repository.branchprop_list.get_property(
498 self.branch.branch_path,
500 SVN_PROP_BZR_FILEIDS, "")
501 existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
505 return dict(map(lambda x: x.split("\t"), existing[len(committed):].splitlines()))
507 def _get_bzr_merges(self):
508 return self.branch.repository.branchprop_list.get_property(
509 self.branch.branch_path,
511 SVN_PROP_BZR_MERGE, "")
513 def _get_svk_merges(self):
514 return self.branch.repository.branchprop_list.get_property(
515 self.branch.branch_path,
517 SVN_PROP_SVK_MERGE, "")
519 def set_pending_merges(self, merges):
520 wc = self._get_wc(write_lock=True)
524 bzr_merge = "\t".join(merges) + "\n"
528 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
529 self._get_bzr_merges() + bzr_merge,
536 svk_merge += revision_id_to_svk_feature(merge) + "\n"
537 except InvalidRevisionId:
540 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
541 self._get_svk_merges() + svk_merge, self.basedir,
546 def add_pending_merge(self, revid):
547 merges = self.pending_merges()
549 self.set_pending_merges(existing)
551 def pending_merges(self):
552 merged = self._get_bzr_merges().splitlines()
555 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
556 if merged_data is None:
559 set_merged = merged_data.splitlines()
563 assert (len(merged) == len(set_merged) or
564 len(merged)+1 == len(set_merged))
566 if len(set_merged) > len(merged):
567 return set_merged[-1].split("\t")
572 class SvnWorkingTreeFormat(WorkingTreeFormat):
573 def get_format_description(self):
574 return "Subversion Working Copy"
576 def initialize(self, a_bzrdir, revision_id=None):
577 raise NotImplementedError(self.initialize)
579 def open(self, a_bzrdir):
580 raise NotImplementedError(self.initialize)
583 class SvnCheckout(BzrDir):
584 """BzrDir implementation for Subversion checkouts (directories
585 containing a .svn subdirectory."""
586 def __init__(self, transport, format):
587 super(SvnCheckout, self).__init__(transport, format)
588 self.local_path = transport.local_abspath(".")
590 # Open related remote repository + branch
591 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
593 svn_url = svn.wc.entry(self.local_path, wc, True).url
597 self.remote_transport = SvnRaTransport(svn_url)
598 self.svn_root_transport = SvnRaTransport(self.remote_transport.get_repos_root())
599 self.root_transport = self.transport = transport
601 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
602 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
603 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
604 if not self.scheme.is_branch(self.branch_path) and not self.scheme.is_tag(self.branch_path):
605 raise NotBranchError(path=self.transport.base)
607 def clone(self, path):
608 raise NotImplementedError(self.clone)
610 def open_workingtree(self, _unsupported=False):
611 return SvnWorkingTree(self, self.local_path, self.open_branch())
613 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
614 # FIXME: honor force_new_repo
615 result = BzrDirFormat.get_default_format().initialize(url)
616 repo = self.find_repository()
617 result_repo = repo.clone(result, revision_id, basis)
618 branch = self.open_branch()
619 branch.sprout(result, revision_id)
620 result.create_workingtree()
623 def open_repository(self):
624 raise NoRepositoryPresent(self)
626 def find_repository(self):
627 return SvnRepository(self, self.svn_root_transport)
629 def create_workingtree(self, revision_id=None):
630 """See BzrDir.create_workingtree().
632 Not implemented for Subversion because having a .svn directory
633 implies having a working copy.
635 raise NotImplementedError(self.create_workingtree)
637 def create_branch(self):
638 """See BzrDir.create_branch()."""
639 raise NotImplementedError(self.create_branch)
641 def open_branch(self, unsupported=True):
642 """See BzrDir.open_branch()."""
643 repos = self.find_repository()
646 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
647 except SubversionException, (msg, num):
648 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
649 raise NotBranchError(path=self.url)
656 class SvnWorkingTreeDirFormat(BzrDirFormat):
657 """Working Tree implementation that uses Subversion working copies."""
658 _lock_class = TransportLock
661 def probe_transport(klass, transport):
664 if isinstance(transport, LocalTransport) and \
665 transport.has(svn.wc.get_adm_dir()):
668 raise NotBranchError(path=transport.base)
670 def _open(self, transport):
671 return SvnCheckout(transport, self)
673 def get_format_string(self):
674 return 'Subversion Local Checkout'
676 def get_format_description(self):
677 return 'Subversion Local Checkout'
679 def initialize_on_transport(self, transport):
680 raise NotImplementedError(self.initialize_on_transport)