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)
57 self.base_revid = branch.repository.generate_revision_id(
58 self.base_revnum, branch.branch_path)
59 self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
61 os.makedirs(self.controldir)
62 os.makedirs(os.path.join(self.controldir, 'lock'))
65 control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
66 self._control_files = LockableFiles(control_transport, 'lock', LockDir)
77 def get_ignore_list(self):
80 def dir_add(wc, prefix):
81 ignores.append(os.path.join(prefix, svn.wc.get_adm_dir()))
82 for pat in svn.wc.get_ignores(svn_config, wc):
83 ignores.append(os.path.join(prefix, pat))
85 entries = svn.wc.entries_read(wc, False)
90 if entries[entry].kind != svn.core.svn_node_dir:
93 subprefix = os.path.join(prefix, entry)
95 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
97 dir_add(subwc, subprefix)
99 svn.wc.adm_close(subwc)
109 def _write_inventory(self, inv):
112 def is_ignored(self, filename):
113 if svn.wc.is_adm_dir(os.path.basename(filename)):
116 (wc, name) = self._get_rel_wc(filename)
119 ignores = svn.wc.get_ignores(svn_config, wc)
120 from fnmatch import fnmatch
121 for pattern in ignores:
122 if fnmatch(name, pattern):
128 def is_control_filename(self, path):
129 return svn.wc.is_adm_dir(path)
131 def remove(self, files, verbose=False, to_file=None):
132 wc = self._get_wc(write_lock=True)
135 svn.wc.delete2(self.abspath(file), wc, None, None, None)
139 def _get_wc(self, relpath="", write_lock=False):
140 return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), write_lock, 0, None)
142 def _get_rel_wc(self, relpath, write_lock=False):
143 dir = os.path.dirname(relpath)
144 file = os.path.basename(relpath)
145 return (self._get_wc(dir, write_lock), file)
147 def move(self, from_paths, to_name):
148 revt = svn.core.svn_opt_revision_t()
149 revt.kind = svn.core.svn_opt_revision_working
150 to_wc = self._get_wc(to_name, write_lock=True)
152 for entry in from_paths:
153 svn.wc.copy(self.abspath(entry), to_wc, os.path.basename(entry), None, None)
155 svn.wc.adm_close(to_wc)
157 for entry in from_paths:
160 def rename_one(self, from_rel, to_rel):
161 revt = svn.core.svn_opt_revision_t()
162 revt.kind = svn.core.svn_opt_revision_unspecified
163 (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
165 svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
166 svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
168 svn.wc.adm_close(to_wc)
170 def read_working_inventory(self):
173 def add_file_to_inv(relpath, id, revid, parent_id):
174 """Add a file to the inventory."""
175 file = InventoryFile(id, os.path.basename(relpath), parent_id)
176 file.revision = revid
178 data = fingerprint_file(open(self.abspath(relpath)))
179 file.text_sha1 = data['sha1']
180 file.text_size = data['size']
181 file.executable = self.is_executable(id, relpath)
184 # Ignore non-existing files
187 def find_copies(url, relpath=""):
188 wc = self._get_wc(relpath)
189 entries = svn.wc.entries_read(wc, False)
190 for entry in entries.values():
191 subrelpath = os.path.join(relpath, entry.name)
192 if entry.name == "" or entry.kind != 'directory':
193 if ((entry.copyfrom_url == url or entry.url == url) and
194 not (entry.schedule in (svn.wc.schedule_delete,
195 svn.wc.schedule_replace))):
197 self.branch.branch_path.strip("/"),
200 find_copies(subrelpath)
204 relpath = entry.url[len(entry.repos):].strip("/")
205 assert entry.schedule in (svn.wc.schedule_normal,
206 svn.wc.schedule_delete,
208 svn.wc.schedule_replace)
209 if entry.schedule == svn.wc.schedule_normal:
210 assert entry.revision >= 0
212 mutter('stay: %r' % relpath)
213 return self.branch.repository.path_to_file_id(entry.revision,
215 elif entry.schedule == svn.wc.schedule_delete:
217 elif (entry.schedule == svn.wc.schedule_add or
218 entry.schedule == svn.wc.schedule_replace):
219 # See if the file this file was copied from disappeared
220 # and has no other copies -> in that case, take id of other file
221 mutter('copies(%r): %r' % (relpath, list(find_copies(entry.copyfrom_url))))
222 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
223 return self.branch.repository.path_to_file_id(entry.copyfrom_rev,
224 entry.copyfrom_url[len(entry.repos):])
225 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(
337 commit_info.revision, self.branch.branch_path)
339 self.base_revid = revid
340 self.branch._revision_history.append(revid)
344 def add(self, files, ids=None):
345 assert isinstance(files, list)
346 wc = self._get_wc(write_lock=True)
350 svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0,
353 svn.wc.prop_set2('bzr:fileid', ids.pop(), relpath, wc,
355 except SubversionException, (_, num):
356 if num == svn.core.SVN_ERR_ENTRY_EXISTS:
358 elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
359 raise NoSuchFile(path=f)
364 def basis_tree(self):
365 if self.base_revid is None or self.base_revid == NULL_REVISION:
366 return self.branch.repository.revision_tree(self.base_revid)
368 return SvnBasisTree(self, self.base_revid)
370 def pull(self, source, overwrite=False, stop_revision=None):
371 if stop_revision is None:
372 stop_revision = self.branch.last_revision()
373 rev = svn.core.svn_opt_revision_t()
374 rev.kind = svn.core.svn_opt_revision_number
375 rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
376 fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
377 self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
378 return fetched-rev.value.number
380 def get_file_sha1(self, file_id, path=None, stat_value=None):
382 path = self._inventory.id2path(file_id)
384 return fingerprint_file(open(self.abspath(path)))['sha1']
386 def _get_bzr_merges(self):
387 return self.branch.repository.branchprop_list.get_property(self.branch.branch_path,
389 SVN_PROP_BZR_MERGE, "")
391 def _get_svk_merges(self):
392 return self.branch.repository.branchprop_list.get_property(self.branch.branch_path,
394 SVN_PROP_SVK_MERGE, "")
396 def set_pending_merges(self, merges):
397 wc = self._get_wc(write_lock=True)
401 bzr_merge = "\t".join(merges) + "\n"
405 svn.wc.prop_set(SVN_PROP_BZR_MERGE,
406 self._get_bzr_merges() + bzr_merge,
413 svk_merge += revision_id_to_svk_feature(merge) + "\n"
414 except InvalidRevisionId:
417 svn.wc.prop_set2(SVN_PROP_SVK_MERGE,
418 self._get_svk_merges() + svk_merge, self.basedir,
423 def add_pending_merge(self, revid):
424 merges = self.pending_merges()
426 self.set_pending_merges(existing)
428 def pending_merges(self):
429 merged = self._get_bzr_merges().splitlines()
432 merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
433 if merged_data is None:
436 set_merged = merged_data.splitlines()
440 assert (len(merged) == len(set_merged) or
441 len(merged)+1 == len(set_merged))
443 if len(set_merged) > len(merged):
444 return set_merged[-1].split("\t")
449 class SvnWorkingTreeFormat(WorkingTreeFormat):
450 def get_format_description(self):
451 return "Subversion Working Copy"
453 def initialize(self, a_bzrdir, revision_id=None):
455 raise NotImplementedError(self.initialize)
457 def open(self, a_bzrdir):
459 raise NotImplementedError(self.initialize)
462 class SvnCheckout(BzrDir):
463 """BzrDir implementation for Subversion checkouts (directories
464 containing a .svn subdirectory."""
465 def __init__(self, transport, format):
466 super(SvnCheckout, self).__init__(transport, format)
467 self.local_path = transport.local_abspath(".")
469 # Open related remote repository + branch
470 wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
472 svn_url = svn.wc.entry(self.local_path, wc, True).url
476 self.remote_transport = SvnRaTransport(svn_url)
477 self.svn_root_transport = self.remote_transport.get_root()
478 self.root_transport = self.transport = transport
480 self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
481 self.scheme = BranchingScheme.guess_scheme(self.branch_path)
482 mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
483 if not self.scheme.is_branch(self.branch_path):
484 raise NotBranchError(path=self.transport.base)
486 def clone(self, path):
487 raise NotImplementedError(self.clone)
489 def open_workingtree(self, _unsupported=False):
490 return SvnWorkingTree(self, self.local_path, self.open_branch())
492 def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
493 # FIXME: honor force_new_repo
494 result = BzrDirFormat.get_default_format().initialize(url)
495 repo = self.open_repository()
496 result_repo = repo.clone(result, revision_id, basis)
497 branch = self.open_branch()
498 branch.sprout(result, revision_id)
499 result.create_workingtree()
502 def open_repository(self):
503 repos = SvnRepository(self, self.svn_root_transport)
506 # Subversion has all-in-one, so a repository is always present
507 find_repository = open_repository
509 def create_workingtree(self, revision_id=None):
510 """See BzrDir.create_workingtree().
512 Not implemented for Subversion because having a .svn directory
513 implies having a working copy.
515 raise NotImplementedError(self.create_workingtree)
517 def create_branch(self):
518 """See BzrDir.create_branch()."""
519 raise NotImplementedError(self.create_branch)
521 def open_branch(self, unsupported=True):
522 """See BzrDir.open_branch()."""
523 repos = self.open_repository()
526 branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
527 except SubversionException, (msg, num):
528 if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
529 raise NotBranchError(path=self.url)
536 class SvnWorkingTreeDirFormat(BzrDirFormat):
537 """Working Tree implementation that uses Subversion working copies."""
538 _lock_class = TransportLock
541 def probe_transport(klass, transport):
544 if isinstance(transport, LocalTransport) and \
545 transport.has(svn.wc.get_adm_dir()):
548 raise NotBranchError(path=transport.base)
550 def _open(self, transport):
551 return SvnCheckout(transport, self)
553 def get_format_string(self):
554 return 'Subversion Local Checkout'
556 def get_format_description(self):
557 return 'Subversion Local Checkout'
559 def initialize_on_transport(self, transport):
560 raise NotImplementedError(self.initialize_on_transport)