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.delta import compare_trees
20 from bzrlib.errors import NotBranchError, NoSuchFile, InvalidRevisionId
21 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile,
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.tree import EmptyTree
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, revision_id_to_svk_feature)
35 from scheme import BranchingScheme
36 from transport import (SvnRaTransport, svn_config,
37 svn_to_bzr_url, bzr_to_svn_url)
38 from tree import SvnBasisTree
42 import svn.core, svn.wc
43 from svn.core import SubversionException
45 class SvnWorkingTree(WorkingTree):
46 """Implementation of WorkingTree that uses a Subversion
47 Working Copy for storage."""
48 def __init__(self, bzrdir, local_path, branch):
49 self._format = SvnWorkingTreeFormat()
50 self.basedir = local_path
54 self.client_ctx = svn.client.create_context()
55 self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
56 self.client_ctx.log_msg_baton2 = self.log_message_func
58 self._set_inventory(self.read_working_inventory())
59 mutter('working inv: %r' % self.read_working_inventory().entries())
61 self.base_revid = branch.repository.generate_revision_id(
62 self.base_revnum, branch.branch_path)
63 mutter('basis inv: %r' % self.basis_tree().inventory.entries())
64 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
66 os.makedirs(self.controldir)
67 os.makedirs(os.path.join(self.controldir, 'lock'))
70 control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
71 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
82 def get_ignore_list(self):
85 def dir_add(wc, prefix):
86 ignores.append(os.path.join(prefix, svn.wc.get_adm_dir()))
87 for pat in svn.wc.get_ignores(svn_config, wc):
88 ignores.append(os.path.join(prefix, pat))
90 entries = svn.wc.entries_read(wc, False)
95 if entries[entry].kind != svn.core.svn_node_dir:
98 subprefix = os.path.join(prefix, entry)
100 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
102 dir_add(subwc, subprefix)
104 svn.wc.adm_close(subwc)
114 def _write_inventory(self, inv):
117 def is_ignored(self, filename):
118 if svn.wc.is_adm_dir(os.path.basename(filename)):
121 (wc, name) = self._get_rel_wc(filename)
124 ignores = svn.wc.get_ignores(svn_config, wc)
125 from fnmatch import fnmatch
126 for pattern in ignores:
127 if fnmatch(name, pattern):
133 def is_control_filename(self, path):
134 return svn.wc.is_adm_dir(path)
136 def remove(self, files, verbose=False, to_file=None):
137 wc = self._get_wc(write_lock=True)
140 svn.wc.delete2(self.abspath(file), wc, None, None, None)
144 def _get_wc(self, relpath="", write_lock=False):
145 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), write_lock, 0, None)
147 def _get_rel_wc(self, relpath, write_lock=False):
148 dir = os.path.dirname(relpath)
149 file = os.path.basename(relpath)
150 return (self._get_wc(dir, write_lock), file)
152 def move(self, from_paths, to_name):
153 revt = svn.core.svn_opt_revision_t()
154 revt.kind = svn.core.svn_opt_revision_working
155 to_wc = self._get_wc(to_name, write_lock=True)
157 for entry in from_paths:
158 svn.wc.copy(self.abspath(entry), to_wc, os.path.basename(entry), None, None)
160 svn.wc.adm_close(to_wc)
162 for entry in from_paths:
165 def rename_one(self, from_rel, to_rel):
166 revt = svn.core.svn_opt_revision_t()
167 revt.kind = svn.core.svn_opt_revision_unspecified
168 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
170 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
171 svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
173 svn.wc.adm_close(to_wc)
175 def read_working_inventory(self):
178 def add_file_to_inv(relpath, id, revid, parent_id):
179 """Add a file to the inventory."""
180 file = InventoryFile(id, os.path.basename(relpath), parent_id)
181 file.revision = revid
183 data = fingerprint_file(open(self.abspath(relpath)))
184 file.text_sha1 = data['sha1']
185 file.text_size = data['size']
186 file.executable = self.is_executable(id, relpath)
189 # Ignore non-existing files
192 def find_copies(url, relpath=""):
193 wc = self._get_wc(relpath)
194 entries = svn.wc.entries_read(wc, False)
195 for entry in entries.values():
196 subrelpath = os.path.join(relpath, entry.name)
197 if entry.name == "" or entry.kind != 'directory':
198 if ((entry.copyfrom_url == url or entry.url == url) and
199 not (entry.schedule in (svn.wc.schedule_delete,
200 svn.wc.schedule_replace))):
202 self.branch.branch_path.strip("/"),
205 find_copies(subrelpath)
209 relpath = entry.url[len(entry.repos):].strip("/")
210 if entry.schedule == svn.wc.schedule_normal:
211 assert entry.revision >= 0
213 mutter('stay: %r' % relpath)
214 return self.branch.repository.path_to_file_id(entry.revision,
216 elif entry.schedule == svn.wc.schedule_delete:
218 elif (entry.schedule == svn.wc.schedule_add or
219 entry.schedule == svn.wc.schedule_replace):
220 # See if the file this file was copied from disappeared
221 # and has no other copies -> in that case, take id of other file
222 mutter('copies(%r): %r' % (relpath, list(find_copies(entry.copyfrom_url))))
223 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
224 return self.branch.repository.path_to_file_id(entry.copyfrom_rev,
225 entry.copyfrom_url[len(entry.repos):])
226 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
230 def add_dir_to_inv(relpath, wc, parent_id):
231 entries = svn.wc.entries_read(wc, False)
235 (id, revid) = find_ids(entry)
240 self.base_revnum = max(self.base_revnum, entry.revision)
242 # First handle directory itself
244 inv.revision_id = revid
246 inventry = InventoryDirectory(id, os.path.basename(relpath), parent_id)
247 inventry.revision = revid
254 subrelpath = os.path.join(relpath, name)
256 entry = entries[name]
259 if entry.kind == svn.core.svn_node_dir:
260 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
263 add_dir_to_inv(subrelpath, subwc, id)
265 svn.wc.adm_close(subwc)
267 (subid, subrevid) = find_ids(entry)
269 self.base_revnum = max(self.base_revnum, entry.revision)
270 add_file_to_inv(subrelpath, subid, subrevid, id)
274 add_dir_to_inv("", wc, None)
280 def set_last_revision(self, revid):
281 mutter('setting last revision to %r' % revid)
282 if revid is None or revid == NULL_REVISION:
283 self.base_revid = revid
286 # TODO: Implement more efficient version
287 newrev = self.branch.repository.get_revision(revid)
288 newrevtree = self.branch.repository.revision_tree(revid)
290 def update_settings(wc, path):
291 id = newrevtree.inventory.path2id(path)
292 mutter("Updating settings for %r" % id)
293 (_, revnum) = self.branch.repository.parse_revision_id(
294 newrevtree.inventory[id].revision)
296 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
298 svn.core.svn_time_to_cstring(newrev.timestamp),
299 newrev.committer, None, False)
301 if newrevtree.inventory[id].kind != 'directory':
304 entries = svn.wc.entries_read(wc, True)
305 for entry in entries:
309 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
311 update_settings(subwc, os.path.join(path, entry))
313 svn.wc.adm_close(subwc)
315 # Set proper version for all files in the wc
316 wc = self._get_wc(write_lock=True)
318 update_settings(wc, "")
321 self.base_revid = revid
324 def log_message_func(self, items, pool):
325 """ Simple log message provider for unit tests. """
328 def commit(self, message=None, revprops=None, timestamp=None, timezone=None, committer=None, rev_id=None, allow_pointless=True,
329 strict=False, verbose=False, local=False, reporter=None, config=None, specific_files=None):
330 assert timestamp is None
331 assert timezone is None
332 assert rev_id is None
335 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
337 specific_files = [self.basedir.encode('utf8')]
339 assert isinstance(message, basestring)
340 self._message = message
342 commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
344 revid = self.branch.repository.generate_revision_id(commit_info.revision, self.branch.branch_path)
346 self.base_revid = revid
347 self.branch._revision_history.append(revid)
351 def add(self, files, ids=None):
352 assert isinstance(files, list)
353 wc = self._get_wc(write_lock=True)
357 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
360 svn.wc.prop_set2('bzr:fileid', ids.pop(), relpath, wc,
362 except SubversionException, (_, num):
363 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
365 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
366 raise NoSuchFile(path=f)
371 def basis_tree(self):
372 if self.base_revid is None or self.base_revid == NULL_REVISION:
375 return SvnBasisTree(self, self.base_revid)
377 def pull(self, source, overwrite=False, stop_revision=None):
378 if stop_revision is None:
379 stop_revision = self.branch.last_revision()
380 rev = svn.core.svn_opt_revision_t()
381 rev.kind = svn.core.svn_opt_revision_number
382 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
383 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
384 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
385 return fetched-rev.value.number
387 def get_file_sha1(self, file_id, path=None):
389 path = self._inventory.id2path(file_id)
391 return fingerprint_file(open(self.abspath(path)))['sha1']
393 def _get_bzr_merges(self):
394 return self.branch.repository._get_dir_prop(self.branch.branch_path,
396 SVN_PROP_BZR_MERGE, "")
398 def _get_svk_merges(self):
399 return self.branch.repository._get_dir_prop(self.branch.branch_path,
401 SVN_PROP_SVK_MERGE, "")
403 def set_pending_merges(self, merges):
404 wc = self._get_wc(write_lock=True)
408 bzr_merge = "\t".join(merges) + "\n"
412 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
413 self._get_bzr_merges() + bzr_merge,
420 svk_merge += revision_id_to_svk_feature(merge) + "\n"
421 except InvalidRevisionId:
424 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
425 self._get_svk_merges() + svk_merge, self.basedir,
430 def add_pending_merge(self, revid):
431 merges = self.pending_merges()
433 self.set_pending_merges(existing)
435 def pending_merges(self):
436 merged = self._get_bzr_merges().splitlines()
439 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
440 if merged_data is None:
443 set_merged = merged_data.splitlines()
447 assert (len(merged) == len(set_merged) or
448 len(merged)+1 == len(set_merged))
450 if len(set_merged) > len(merged):
451 return set_merged[-1].split("\t")
456 class SvnWorkingTreeFormat(WorkingTreeFormat):
457 def get_format_description(self):
458 return "Subversion Working Copy"
460 def initialize(self, a_bzrdir, revision_id=None):
462 raise NotImplementedError(self.initialize)
464 def open(self, a_bzrdir):
466 raise NotImplementedError(self.initialize)
469 class SvnCheckout(BzrDir):
470 def __init__(self, transport, format):
471 super(SvnCheckout, self).__init__(transport, format)
472 self.local_path = transport.local_abspath(".")
474 # Open related remote repository + branch
475 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
477 svn_url = svn.wc.entry(self.local_path, wc, True).url
481 bzr_url = svn_to_bzr_url(svn_url)
483 self.remote_transport = SvnRaTransport(svn_url)
484 self.svn_root_transport = self.remote_transport.get_root()
485 self.root_transport = self.transport = transport
487 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
488 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
489 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
490 if not self.scheme.is_branch(self.branch_path):
491 raise NotBranchError(path=self.transport.base)
493 def clone(self, path):
494 raise NotImplementedError(self.clone)
496 def open_workingtree(self, _unsupported=False):
497 return SvnWorkingTree(self, self.local_path, self.open_branch())
499 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
500 # FIXME: honor force_new_repo
501 result = BzrDirFormat.get_default_format().initialize(url)
502 repo = self.open_repository()
503 result_repo = repo.clone(result, revision_id, basis)
504 branch = self.open_branch()
505 branch.sprout(result, revision_id)
506 result.create_workingtree()
509 def open_repository(self):
510 repos = SvnRepository(self, self.svn_root_transport)
511 repos._format = self._format
514 # Subversion has all-in-one, so a repository is always present
515 find_repository = open_repository
517 def create_workingtree(self, revision_id=None):
518 raise NotImplementedError(self.create_workingtree)
520 def create_branch(self):
521 """See BzrDir.create_branch()."""
522 raise NotImplementedError(self.create_branch)
524 def open_branch(self, unsupported=True):
525 """See BzrDir.open_branch()."""
526 repos = self.open_repository()
529 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
530 except SubversionException, (msg, num):
531 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
532 raise NotBranchError(path=self.url)
539 class SvnWorkingTreeDirFormat(BzrDirFormat):
540 _lock_class = TransportLock
543 def probe_transport(klass, transport):
546 if transport.has(svn.wc.get_adm_dir()):
549 raise NotBranchError(path=transport.base)
551 def _open(self, transport):
552 return SvnCheckout(transport, self)
554 def get_format_string(self):
555 return 'Subversion Local Checkout'
557 def get_format_description(self):
558 return 'Subversion Local Checkout'
560 def initialize_on_transport(self, transport):
561 raise NotImplementedError(self.initialize_on_transport)