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
298 inv.revision_id = revid
304 subrelpath = os.path.join(relpath, name)
306 entry = entries[name]
309 if entry.kind == svn.core.svn_node_dir:
310 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
313 add_dir_to_inv(subrelpath, subwc, id)
315 svn.wc.adm_close(subwc)
317 (subid, subrevid) = find_ids(entry, rootwc)
319 add_file_to_inv(subrelpath, subid, subrevid, id)
321 mutter('no id for %r' % entry.url)
323 rootwc = self._get_wc()
325 add_dir_to_inv("", rootwc, None)
327 svn.wc.adm_close(rootwc)
329 self._set_inventory(inv, dirty=False)
332 def set_last_revision(self, revid):
333 mutter('setting last revision to %r' % revid)
334 if revid is None or revid == NULL_REVISION:
335 self.base_revid = revid
337 self.base_tree = RevisionTree(self, Inventory(), revid)
340 (bp, rev) = self.branch.repository.parse_revision_id(revid)
341 assert bp == self.branch.branch_path
342 self.base_revnum = rev
343 self.base_revid = revid
344 self.base_tree = SvnBasisTree(self)
346 # TODO: Implement more efficient version
347 newrev = self.branch.repository.get_revision(revid)
348 newrevtree = self.branch.repository.revision_tree(revid)
350 def update_settings(wc, path):
351 id = newrevtree.inventory.path2id(path)
352 mutter("Updating settings for %r" % id)
353 (_, revnum) = self.branch.repository.parse_revision_id(
354 newrevtree.inventory[id].revision)
356 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
358 svn.core.svn_time_to_cstring(newrev.timestamp),
359 newrev.committer, None, False)
361 if newrevtree.inventory[id].kind != 'directory':
364 entries = svn.wc.entries_read(wc, True)
365 for entry in entries:
369 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
371 update_settings(subwc, os.path.join(path, entry))
373 svn.wc.adm_close(subwc)
375 # Set proper version for all files in the wc
376 wc = self._get_wc(write_lock=True)
378 update_settings(wc, "")
381 self.base_revid = revid
383 def commit(self, message=None, message_callback=None, revprops=None, timestamp=None, timezone=None, committer=None,
384 rev_id=None, allow_pointless=True, strict=False, verbose=False, local=False, reporter=None, config=None,
385 specific_files=None):
386 assert timestamp is None
387 assert timezone is None
388 assert rev_id is None
391 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
393 specific_files = [self.basedir.encode('utf8')]
395 if message_callback is not None:
396 def log_message_func(items, pool):
397 """ Simple log message provider for unit tests. """
398 return message_callback(self).encode("utf-8")
400 assert isinstance(message, basestring)
401 def log_message_func(items, pool):
402 """ Simple log message provider for unit tests. """
403 return message.encode("utf-8")
405 self.client_ctx.log_msg_baton2 = log_message_func
406 commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
407 self.client_ctx.log_msg_baton2 = None
409 revid = self.branch.repository.generate_revision_id(
410 commit_info.revision, self.branch.branch_path)
412 self.base_revid = revid
413 self.base_revnum = commit_info.revision
414 self.base_tree = SvnBasisTree(self)
416 #FIXME: Use public API:
417 self.branch.revision_history()
418 self.branch._revision_history.append(revid)
422 def add(self, files, ids=None):
423 if isinstance(files, str):
425 if isinstance(ids, str):
430 assert isinstance(files, list)
433 wc = self._get_wc(os.path.dirname(f), write_lock=True)
435 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
438 self._change_fileid_mapping(ids.pop(), f, wc)
439 except SubversionException, (_, num):
440 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
442 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
443 raise NoSuchFile(path=f)
447 self.read_working_inventory()
449 def basis_tree(self):
450 if self.base_revid is None or self.base_revid == NULL_REVISION:
451 return self.branch.repository.revision_tree(self.base_revid)
453 return self.base_tree
455 def pull(self, source, overwrite=False, stop_revision=None, delta_reporter=None):
456 result = PullResult()
457 result.source_branch = source
458 result.master_branch = None
459 result.target_branch = self.branch
460 (result.old_revno, result.old_revid) = self.branch.last_revision_info()
461 if stop_revision is None:
462 stop_revision = self.branch.last_revision()
463 rev = svn.core.svn_opt_revision_t()
464 rev.kind = svn.core.svn_opt_revision_number
465 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
466 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
467 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
468 result.new_revid = self.branch.generate_revision_id(fetched)
469 result.new_revno = self.branch.revision_id_to_revno(result.new_revid)
472 def get_file_sha1(self, file_id, path=None, stat_value=None):
474 path = self._inventory.id2path(file_id)
475 return fingerprint_file(open(self.abspath(path)))['sha1']
477 def _change_fileid_mapping(self, id, path, wc=None):
479 subwc = self._get_wc(write_lock=True)
482 new_entries = self._get_new_file_ids(subwc)
484 if new_entries.has_key(path):
485 del new_entries[path]
487 new_entries[path] = id
488 committed = self.branch.repository.branchprop_list.get_property(
489 self.branch.branch_path,
491 SVN_PROP_BZR_FILEIDS, "")
492 existing = committed + "".join(map(lambda (path, id): "%s\t%s\n" % (path, id), new_entries.items()))
494 svn.wc.prop_set(SVN_PROP_BZR_FILEIDS, existing.encode("utf-8"), self.basedir, subwc)
496 svn.wc.adm_close(subwc)
498 def _get_new_file_ids(self, wc):
499 committed = self.branch.repository.branchprop_list.get_property(
500 self.branch.branch_path,
502 SVN_PROP_BZR_FILEIDS, "")
503 existing = svn.wc.prop_get(SVN_PROP_BZR_FILEIDS, self.basedir, wc)
507 return dict(map(lambda x: x.split("\t"), existing[len(committed):].splitlines()))
509 def _get_bzr_merges(self):
510 return self.branch.repository.branchprop_list.get_property(
511 self.branch.branch_path,
513 SVN_PROP_BZR_MERGE, "")
515 def _get_svk_merges(self):
516 return self.branch.repository.branchprop_list.get_property(
517 self.branch.branch_path,
519 SVN_PROP_SVK_MERGE, "")
521 def set_pending_merges(self, merges):
522 wc = self._get_wc(write_lock=True)
526 bzr_merge = "\t".join(merges) + "\n"
530 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
531 self._get_bzr_merges() + bzr_merge,
538 svk_merge += revision_id_to_svk_feature(merge) + "\n"
539 except InvalidRevisionId:
542 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
543 self._get_svk_merges() + svk_merge, self.basedir,
548 def add_pending_merge(self, revid):
549 merges = self.pending_merges()
551 self.set_pending_merges(existing)
553 def pending_merges(self):
554 merged = self._get_bzr_merges().splitlines()
557 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
558 if merged_data is None:
561 set_merged = merged_data.splitlines()
565 assert (len(merged) == len(set_merged) or
566 len(merged)+1 == len(set_merged))
568 if len(set_merged) > len(merged):
569 return set_merged[-1].split("\t")
574 class SvnWorkingTreeFormat(WorkingTreeFormat):
575 def get_format_description(self):
576 return "Subversion Working Copy"
578 def initialize(self, a_bzrdir, revision_id=None):
579 raise NotImplementedError(self.initialize)
581 def open(self, a_bzrdir):
582 raise NotImplementedError(self.initialize)
585 class SvnCheckout(BzrDir):
586 """BzrDir implementation for Subversion checkouts (directories
587 containing a .svn subdirectory."""
588 def __init__(self, transport, format):
589 super(SvnCheckout, self).__init__(transport, format)
590 self.local_path = transport.local_abspath(".")
592 # Open related remote repository + branch
593 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
595 svn_url = svn.wc.entry(self.local_path, wc, True).url
599 self.remote_transport = SvnRaTransport(svn_url)
600 self.svn_root_transport = SvnRaTransport(self.remote_transport.get_repos_root())
601 self.root_transport = self.transport = transport
603 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
604 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
605 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
606 if not self.scheme.is_branch(self.branch_path) and not self.scheme.is_tag(self.branch_path):
607 raise NotBranchError(path=self.transport.base)
609 def clone(self, path):
610 raise NotImplementedError(self.clone)
612 def open_workingtree(self, _unsupported=False):
613 return SvnWorkingTree(self, self.local_path, self.open_branch())
615 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
616 # FIXME: honor force_new_repo
617 result = BzrDirFormat.get_default_format().initialize(url)
618 repo = self.find_repository()
619 result_repo = repo.clone(result, revision_id, basis)
620 branch = self.open_branch()
621 branch.sprout(result, revision_id)
622 result.create_workingtree()
625 def open_repository(self):
626 raise NoRepositoryPresent(self)
628 def find_repository(self):
629 return SvnRepository(self, self.svn_root_transport)
631 def create_workingtree(self, revision_id=None):
632 """See BzrDir.create_workingtree().
634 Not implemented for Subversion because having a .svn directory
635 implies having a working copy.
637 raise NotImplementedError(self.create_workingtree)
639 def create_branch(self):
640 """See BzrDir.create_branch()."""
641 raise NotImplementedError(self.create_branch)
643 def open_branch(self, unsupported=True):
644 """See BzrDir.open_branch()."""
645 repos = self.find_repository()
648 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
649 except SubversionException, (msg, num):
650 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
651 raise NotBranchError(path=self.url)
658 class SvnWorkingTreeDirFormat(BzrDirFormat):
659 """Working Tree implementation that uses Subversion working copies."""
660 _lock_class = TransportLock
663 def probe_transport(klass, transport):
666 if isinstance(transport, LocalTransport) and \
667 transport.has(svn.wc.get_adm_dir()):
670 raise NotBranchError(path=transport.base)
672 def _open(self, transport):
673 return SvnCheckout(transport, self)
675 def get_format_string(self):
676 return 'Subversion Local Checkout'
678 def get_format_description(self):
679 return 'Subversion Local Checkout'
681 def initialize_on_transport(self, transport):
682 raise NotImplementedError(self.initialize_on_transport)