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 NotBranchError, NoSuchFile, InvalidRevisionId
20 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile)
21 from bzrlib.lockable_files import TransportLock, LockableFiles
22 from bzrlib.lockdir import LockDir
23 from bzrlib.osutils import rand_bytes, fingerprint_file
24 from bzrlib.progress import DummyProgress
25 from bzrlib.revision import NULL_REVISION
26 from bzrlib.trace import mutter
27 from bzrlib.transport.local import LocalTransport
28 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
30 from branch import SvnBranch
31 from repository import (SvnRepository, escape_svn_path, SVN_PROP_BZR_MERGE,
32 SVN_PROP_SVK_MERGE, revision_id_to_svk_feature)
33 from scheme import BranchingScheme
34 from transport import (SvnRaTransport, svn_config, bzr_to_svn_url)
35 from tree import SvnBasisTree
39 import svn.core, svn.wc
40 from svn.core import SubversionException
42 class SvnWorkingTree(WorkingTree):
43 """Implementation of WorkingTree that uses a Subversion
44 Working Copy for storage."""
45 def __init__(self, bzrdir, local_path, branch):
46 self._format = SvnWorkingTreeFormat()
47 self.basedir = local_path
51 self.client_ctx = svn.client.create_context()
52 self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
53 self.client_ctx.log_msg_baton2 = self.log_message_func
55 self._set_inventory(self.read_working_inventory(), dirty=False)
56 mutter('working inv: %r' % self.read_working_inventory().entries())
58 self.base_revid = branch.repository.generate_revision_id(
59 self.base_revnum, branch.branch_path)
60 mutter('basis inv: %r' % self.basis_tree().inventory.entries())
61 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
63 os.makedirs(self.controldir)
64 os.makedirs(os.path.join(self.controldir, 'lock'))
67 control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
68 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
79 def get_ignore_list(self):
82 def dir_add(wc, prefix):
83 ignores.append(os.path.join(prefix, svn.wc.get_adm_dir()))
84 for pat in svn.wc.get_ignores(svn_config, wc):
85 ignores.append(os.path.join(prefix, pat))
87 entries = svn.wc.entries_read(wc, False)
92 if entries[entry].kind != svn.core.svn_node_dir:
95 subprefix = os.path.join(prefix, entry)
97 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
99 dir_add(subwc, subprefix)
101 svn.wc.adm_close(subwc)
111 def _write_inventory(self, inv):
114 def is_ignored(self, filename):
115 if svn.wc.is_adm_dir(os.path.basename(filename)):
118 (wc, name) = self._get_rel_wc(filename)
121 ignores = svn.wc.get_ignores(svn_config, wc)
122 from fnmatch import fnmatch
123 for pattern in ignores:
124 if fnmatch(name, pattern):
130 def is_control_filename(self, path):
131 return svn.wc.is_adm_dir(path)
133 def remove(self, files, verbose=False, to_file=None):
134 wc = self._get_wc(write_lock=True)
137 svn.wc.delete2(self.abspath(file), wc, None, None, None)
141 def _get_wc(self, relpath="", write_lock=False):
142 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), write_lock, 0, None)
144 def _get_rel_wc(self, relpath, write_lock=False):
145 dir = os.path.dirname(relpath)
146 file = os.path.basename(relpath)
147 return (self._get_wc(dir, write_lock), file)
149 def move(self, from_paths, to_name):
150 revt = svn.core.svn_opt_revision_t()
151 revt.kind = svn.core.svn_opt_revision_working
152 to_wc = self._get_wc(to_name, write_lock=True)
154 for entry in from_paths:
155 svn.wc.copy(self.abspath(entry), to_wc, os.path.basename(entry), None, None)
157 svn.wc.adm_close(to_wc)
159 for entry in from_paths:
162 def rename_one(self, from_rel, to_rel):
163 revt = svn.core.svn_opt_revision_t()
164 revt.kind = svn.core.svn_opt_revision_unspecified
165 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
167 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
168 svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
170 svn.wc.adm_close(to_wc)
172 def read_working_inventory(self):
175 def add_file_to_inv(relpath, id, revid, parent_id):
176 """Add a file to the inventory."""
177 file = InventoryFile(id, os.path.basename(relpath), parent_id)
178 file.revision = revid
180 data = fingerprint_file(open(self.abspath(relpath)))
181 file.text_sha1 = data['sha1']
182 file.text_size = data['size']
183 file.executable = self.is_executable(id, relpath)
186 # Ignore non-existing files
189 def find_copies(url, relpath=""):
190 wc = self._get_wc(relpath)
191 entries = svn.wc.entries_read(wc, False)
192 for entry in entries.values():
193 subrelpath = os.path.join(relpath, entry.name)
194 if entry.name == "" or entry.kind != 'directory':
195 if ((entry.copyfrom_url == url or entry.url == url) and
196 not (entry.schedule in (svn.wc.schedule_delete,
197 svn.wc.schedule_replace))):
199 self.branch.branch_path.strip("/"),
202 find_copies(subrelpath)
206 relpath = entry.url[len(entry.repos):].strip("/")
207 if entry.schedule == svn.wc.schedule_normal:
208 assert entry.revision >= 0
210 mutter('stay: %r' % relpath)
211 return self.branch.repository.path_to_file_id(entry.revision,
213 elif entry.schedule == svn.wc.schedule_delete:
215 elif (entry.schedule == svn.wc.schedule_add or
216 entry.schedule == svn.wc.schedule_replace):
217 # See if the file this file was copied from disappeared
218 # and has no other copies -> in that case, take id of other file
219 mutter('copies(%r): %r' % (relpath, list(find_copies(entry.copyfrom_url))))
220 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
221 return self.branch.repository.path_to_file_id(entry.copyfrom_rev,
222 entry.copyfrom_url[len(entry.repos):])
223 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
227 def add_dir_to_inv(relpath, wc, parent_id):
228 entries = svn.wc.entries_read(wc, False)
232 (id, revid) = find_ids(entry)
237 self.base_revnum = max(self.base_revnum, entry.revision)
239 # First handle directory itself
240 inv.add_path(relpath, 'directory', id, parent_id).revision = revid
246 subrelpath = os.path.join(relpath, name)
248 entry = entries[name]
251 if entry.kind == svn.core.svn_node_dir:
252 subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath),
255 add_dir_to_inv(subrelpath, subwc, id)
257 svn.wc.adm_close(subwc)
259 (subid, subrevid) = find_ids(entry)
261 self.base_revnum = max(self.base_revnum, entry.revision)
262 add_file_to_inv(subrelpath, subid, subrevid, id)
266 add_dir_to_inv("", wc, None)
272 def set_last_revision(self, revid):
273 mutter('setting last revision to %r' % revid)
274 if revid is None or revid == NULL_REVISION:
275 self.base_revid = revid
278 # TODO: Implement more efficient version
279 newrev = self.branch.repository.get_revision(revid)
280 newrevtree = self.branch.repository.revision_tree(revid)
282 def update_settings(wc, path):
283 id = newrevtree.inventory.path2id(path)
284 mutter("Updating settings for %r" % id)
285 (_, revnum) = self.branch.repository.parse_revision_id(
286 newrevtree.inventory[id].revision)
288 svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc,
290 svn.core.svn_time_to_cstring(newrev.timestamp),
291 newrev.committer, None, False)
293 if newrevtree.inventory[id].kind != 'directory':
296 entries = svn.wc.entries_read(wc, True)
297 for entry in entries:
301 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
303 update_settings(subwc, os.path.join(path, entry))
305 svn.wc.adm_close(subwc)
307 # Set proper version for all files in the wc
308 wc = self._get_wc(write_lock=True)
310 update_settings(wc, "")
313 self.base_revid = revid
316 def log_message_func(self, items, pool):
317 """ Simple log message provider for unit tests. """
320 def commit(self, message=None, revprops=None, timestamp=None, timezone=None, committer=None, rev_id=None, allow_pointless=True,
321 strict=False, verbose=False, local=False, reporter=None, config=None, specific_files=None):
322 assert timestamp is None
323 assert timezone is None
324 assert rev_id is None
327 specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
329 specific_files = [self.basedir.encode('utf8')]
331 assert isinstance(message, basestring)
332 self._message = message
334 commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
336 revid = self.branch.repository.generate_revision_id(commit_info.revision, self.branch.branch_path)
338 self.base_revid = revid
339 self.branch._revision_history.append(revid)
343 def add(self, files, ids=None):
344 assert isinstance(files, list)
345 wc = self._get_wc(write_lock=True)
349 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
352 svn.wc.prop_set2('bzr:fileid', ids.pop(), relpath, wc,
354 except SubversionException, (_, num):
355 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
357 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
358 raise NoSuchFile(path=f)
363 def basis_tree(self):
364 if self.base_revid is None or self.base_revid == NULL_REVISION:
365 return self.branch.repository.revision_tree(self.base_revid)
367 return SvnBasisTree(self, self.base_revid)
369 def pull(self, source, overwrite=False, stop_revision=None):
370 if stop_revision is None:
371 stop_revision = self.branch.last_revision()
372 rev = svn.core.svn_opt_revision_t()
373 rev.kind = svn.core.svn_opt_revision_number
374 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
375 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
376 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
377 return fetched-rev.value.number
379 def get_file_sha1(self, file_id, path=None, stat_value=None):
381 path = self._inventory.id2path(file_id)
383 return fingerprint_file(open(self.abspath(path)))['sha1']
385 def _get_bzr_merges(self):
386 return self.branch.repository._get_branch_prop(self.branch.branch_path,
388 SVN_PROP_BZR_MERGE, "")
390 def _get_svk_merges(self):
391 return self.branch.repository._get_branch_prop(self.branch.branch_path,
393 SVN_PROP_SVK_MERGE, "")
395 def set_pending_merges(self, merges):
396 wc = self._get_wc(write_lock=True)
400 bzr_merge = "\t".join(merges) + "\n"
404 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
405 self._get_bzr_merges() + bzr_merge,
412 svk_merge += revision_id_to_svk_feature(merge) + "\n"
413 except InvalidRevisionId:
416 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
417 self._get_svk_merges() + svk_merge, self.basedir,
422 def add_pending_merge(self, revid):
423 merges = self.pending_merges()
425 self.set_pending_merges(existing)
427 def pending_merges(self):
428 merged = self._get_bzr_merges().splitlines()
431 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
432 if merged_data is None:
435 set_merged = merged_data.splitlines()
439 assert (len(merged) == len(set_merged) or
440 len(merged)+1 == len(set_merged))
442 if len(set_merged) > len(merged):
443 return set_merged[-1].split("\t")
448 class SvnWorkingTreeFormat(WorkingTreeFormat):
449 def get_format_description(self):
450 return "Subversion Working Copy"
452 def initialize(self, a_bzrdir, revision_id=None):
454 raise NotImplementedError(self.initialize)
456 def open(self, a_bzrdir):
458 raise NotImplementedError(self.initialize)
461 class SvnCheckout(BzrDir):
462 """BzrDir implementation for Subversion checkouts (directories
463 containing a .svn subdirectory."""
464 def __init__(self, transport, format):
465 super(SvnCheckout, self).__init__(transport, format)
466 self.local_path = transport.local_abspath(".")
468 # Open related remote repository + branch
469 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
471 svn_url = svn.wc.entry(self.local_path, wc, True).url
475 self.remote_transport = SvnRaTransport(svn_url)
476 self.svn_root_transport = self.remote_transport.get_root()
477 self.root_transport = self.transport = transport
479 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
480 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
481 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
482 if not self.scheme.is_branch(self.branch_path):
483 raise NotBranchError(path=self.transport.base)
485 def clone(self, path):
486 raise NotImplementedError(self.clone)
488 def open_workingtree(self, _unsupported=False):
489 return SvnWorkingTree(self, self.local_path, self.open_branch())
491 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
492 # FIXME: honor force_new_repo
493 result = BzrDirFormat.get_default_format().initialize(url)
494 repo = self.open_repository()
495 result_repo = repo.clone(result, revision_id, basis)
496 branch = self.open_branch()
497 branch.sprout(result, revision_id)
498 result.create_workingtree()
501 def open_repository(self):
502 repos = SvnRepository(self, self.svn_root_transport)
505 # Subversion has all-in-one, so a repository is always present
506 find_repository = open_repository
508 def create_workingtree(self, revision_id=None):
509 """See BzrDir.create_workingtree().
511 Not implemented for Subversion because having a .svn directory
512 implies having a working copy.
514 raise NotImplementedError(self.create_workingtree)
516 def create_branch(self):
517 """See BzrDir.create_branch()."""
518 raise NotImplementedError(self.create_branch)
520 def open_branch(self, unsupported=True):
521 """See BzrDir.open_branch()."""
522 repos = self.open_repository()
525 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
526 except SubversionException, (msg, num):
527 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
528 raise NotBranchError(path=self.url)
535 class SvnWorkingTreeDirFormat(BzrDirFormat):
536 """Working Tree implementation that uses Subversion working copies."""
537 _lock_class = TransportLock
540 def probe_transport(klass, transport):
543 if isinstance(transport, LocalTransport) and \
544 transport.has(svn.wc.get_adm_dir()):
547 raise NotBranchError(path=transport.base)
549 def _open(self, transport):
550 return SvnCheckout(transport, self)
552 def get_format_string(self):
553 return 'Subversion Local Checkout'
555 def get_format_description(self):
556 return 'Subversion Local Checkout'
558 def initialize_on_transport(self, transport):
559 raise NotImplementedError(self.initialize_on_transport)