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 3 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
16 """Access to stored Subversion basis trees."""
18 from bzrlib import osutils, urlutils
19 from bzrlib.branch import Branch
20 from bzrlib.inventory import Inventory, InventoryDirectory, TreeReference
22 from bzrlib.revision import CURRENT_REVISION
23 from bzrlib.trace import mutter
24 from bzrlib.revisiontree import RevisionTree
28 from cStringIO import StringIO
31 import svn.core, svn.wc, svn.delta
32 from svn.core import Pool
34 from bzrlib.plugins.svn import errors, properties
36 def parse_externals_description(base_url, val):
37 """Parse an svn:externals property value.
39 :param base_url: URL on which the property is set. Used for
42 :returns: dictionary with local names as keys, (revnum, url)
43 as value. revnum is the revision number and is
44 set to None if not applicable.
47 for l in val.splitlines():
48 if l == "" or l[0] == "#":
50 pts = l.rsplit(None, 2)
52 if not pts[1].startswith("-r"):
53 raise errors.InvalidExternalsDescription()
54 ret[pts[0]] = (int(pts[1][2:]), urlutils.join(base_url, pts[2]))
56 if pts[1].startswith("//"):
57 raise NotImplementedError("Relative to the scheme externals not yet supported")
58 if pts[1].startswith("^/"):
59 raise NotImplementedError("Relative to the repository root externals not yet supported")
60 ret[pts[0]] = (None, urlutils.join(base_url, pts[1]))
62 raise errors.InvalidExternalsDescription()
66 def inventory_add_external(inv, parent_id, path, revid, ref_revnum, url):
67 """Add an svn:externals entry to an inventory as a tree-reference.
69 :param inv: Inventory to add to.
70 :param parent_id: File id of directory the entry was set on.
71 :param path: Path of the entry, relative to entry with parent_id.
72 :param revid: Revision to store in newly created inventory entries.
73 :param ref_revnum: Referenced revision of tree that's being referenced, or
74 None if no specific revision is being referenced.
75 :param url: URL of referenced tree.
77 assert ref_revnum is None or isinstance(ref_revnum, int)
78 assert revid is None or isinstance(revid, str)
79 (dir, name) = os.path.split(path)
80 parent = inv[parent_id]
82 for part in dir.split("/"):
83 if parent.children.has_key(part):
84 parent = parent.children[part]
86 # Implicitly add directory if it doesn't exist yet
87 # TODO: Generate a file id
88 parent = inv.add(InventoryDirectory('someid', part,
89 parent_id=parent.file_id))
90 parent.revision = revid
92 reference_branch = Branch.open(url)
93 file_id = reference_branch.get_root_id()
94 ie = TreeReference(file_id, name, parent.file_id, revision=revid)
95 if ref_revnum is not None:
96 ie.reference_revision = reference_branch.get_rev_id(ref_revnum)
100 # Deal with Subversion 1.5 and the patched Subversion 1.4 (which are
101 # slightly different).
103 if hasattr(svn.delta, 'tx_invoke_window_handler'):
104 def apply_txdelta_handler(src_stream, target_stream, pool):
105 assert hasattr(src_stream, 'read')
106 assert hasattr(target_stream, 'write')
107 window_handler, baton = svn.delta.tx_apply(src_stream, target_stream,
111 window_handler(window, baton)
115 def apply_txdelta_handler(src_stream, target_stream, pool):
116 assert hasattr(src_stream, 'read')
117 assert hasattr(target_stream, 'write')
118 ret = svn.delta.svn_txdelta_apply(src_stream, target_stream, None, pool)
121 svn.delta.invoke_txdelta_window_handler(
122 ret[1], window, ret[2])
127 class SvnRevisionTree(RevisionTree):
128 """A tree that existed in a historical Subversion revision."""
129 def __init__(self, repository, revision_id):
130 self._repository = repository
131 self._revision_id = revision_id
133 (self.branch_path, self.revnum, mapping) = repository.lookup_revision_id(revision_id)
134 self._inventory = Inventory()
135 self.id_map = repository.get_fileid_map(self.revnum, self.branch_path,
137 editor = TreeBuildEditor(self, pool)
139 root_repos = repository.transport.get_svn_repos_root()
140 reporter = repository.transport.do_switch(
142 urlutils.join(root_repos, self.branch_path), editor, pool)
143 reporter.set_path("", 0, True, None, pool)
144 reporter.finish_report(pool)
147 def get_file_lines(self, file_id):
148 return osutils.split_lines(self.file_data[file_id])
151 class TreeBuildEditor(svn.delta.Editor):
152 """Builds a tree given Subversion tree transform calls."""
153 def __init__(self, tree, pool):
155 self.repository = tree._repository
156 self.last_revnum = {}
158 self.dir_ignores = {}
161 def set_target_revision(self, revnum):
164 def open_root(self, revnum, baton):
165 file_id, revision_id = self.tree.id_map[""]
166 ie = self.tree._inventory.add_path("", 'directory', file_id)
167 ie.revision = revision_id
168 self.tree._inventory.revision_id = revision_id
171 def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
172 path = path.decode("utf-8")
173 file_id, revision_id = self.tree.id_map[path]
174 ie = self.tree._inventory.add_path(path, 'directory', file_id)
175 ie.revision = revision_id
178 def change_dir_prop(self, id, name, value, pool):
179 if name == properties.PROP_ENTRY_COMMITTED_REV:
180 self.dir_revnum[id] = int(value)
181 elif name == properties.PROP_IGNORE:
182 self.dir_ignores[id] = value
183 elif name in (properties.PROP_ENTRY_COMMITTED_DATE,
184 properties.PROP_ENTRY_LAST_AUTHOR,
185 properties.PROP_ENTRY_LOCK_TOKEN,
186 properties.PROP_ENTRY_UUID,
187 properties.PROP_EXECUTABLE):
189 elif name.startswith(properties.PROP_WC_PREFIX):
191 elif name.startswith(properties.PROP_PREFIX):
192 mutter('unsupported dir property %r' % name)
194 def change_file_prop(self, id, name, value, pool):
195 if name == properties.PROP_EXECUTABLE:
196 self.is_executable = (value != None)
197 elif name == properties.PROP_SPECIAL:
198 self.is_symlink = (value != None)
199 elif name == properties.PROP_EXTERNALS:
200 mutter('%r property on file!' % name)
201 elif name == properties.PROP_ENTRY_COMMITTED_REV:
202 self.last_file_rev = int(value)
203 elif name in (properties.PROP_ENTRY_COMMITTED_DATE,
204 properties.PROP_ENTRY_LAST_AUTHOR,
205 properties.PROP_ENTRY_LOCK_TOKEN,
206 properties.PROP_ENTRY_UUID,
207 properties.PROP_MIME_TYPE):
209 elif name.startswith(properties.PROP_WC_PREFIX):
211 elif name.startswith(properties.PROP_PREFIX):
212 mutter('unsupported file property %r' % name)
214 def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
215 path = path.decode("utf-8")
216 self.is_symlink = False
217 self.is_executable = False
220 def close_dir(self, id):
221 if id in self.tree._inventory and self.dir_ignores.has_key(id):
222 self.tree._inventory[id].ignores = self.dir_ignores[id]
224 def close_file(self, path, checksum):
225 file_id, revision_id = self.tree.id_map[path]
227 ie = self.tree._inventory.add_path(path, 'symlink', file_id)
229 ie = self.tree._inventory.add_path(path, 'file', file_id)
230 ie.revision = revision_id
233 self.file_stream.seek(0)
234 file_data = self.file_stream.read()
238 actual_checksum = md5.new(file_data).hexdigest()
239 assert(checksum is None or checksum == actual_checksum,
240 "checksum mismatch: %r != %r" % (checksum, actual_checksum))
243 ie.symlink_target = file_data[len("link "):]
247 ie.executable = False
249 ie.text_sha1 = osutils.sha_string(file_data)
250 ie.text_size = len(file_data)
251 self.tree.file_data[file_id] = file_data
252 ie.executable = self.is_executable
254 self.file_stream = None
256 def close_edit(self):
259 def abort_edit(self):
262 def apply_textdelta(self, file_id, base_checksum):
263 self.file_stream = StringIO()
264 return apply_txdelta_handler(StringIO(""), self.file_stream, self.pool)
267 class SvnBasisTree(RevisionTree):
268 """Optimized version of SvnRevisionTree."""
269 def __init__(self, workingtree):
270 self.workingtree = workingtree
271 self._revision_id = workingtree.branch.generate_revision_id(
272 workingtree.base_revnum)
273 self.id_map = workingtree.branch.repository.get_fileid_map(
274 workingtree.base_revnum,
275 workingtree.branch.get_branch_path(workingtree.base_revnum),
276 workingtree.branch.mapping)
277 self._inventory = Inventory(root_id=None)
278 self._repository = workingtree.branch.repository
280 def add_file_to_inv(relpath, id, revid, wc):
281 props = svn.wc.get_prop_diffs(self.workingtree.abspath(relpath).encode("utf-8"), wc)
282 if isinstance(props, list): # Subversion 1.5
284 if props.has_key(properties.PROP_SPECIAL):
285 ie = self._inventory.add_path(relpath, 'symlink', id)
286 ie.symlink_target = open(self._abspath(relpath)).read()[len("link "):]
290 ie.executable = False
292 ie = self._inventory.add_path(relpath, 'file', id)
293 data = osutils.fingerprint_file(open(self._abspath(relpath)))
294 ie.text_sha1 = data['sha1']
295 ie.text_size = data['size']
296 ie.executable = props.has_key(properties.PROP_EXECUTABLE)
301 relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
302 if entry.schedule in (svn.wc.schedule_normal,
303 svn.wc.schedule_delete,
304 svn.wc.schedule_replace):
305 return self.id_map[workingtree.branch.unprefix(relpath.decode("utf-8"))]
308 def add_dir_to_inv(relpath, wc, parent_id):
309 entries = svn.wc.entries_read(wc, False)
311 (id, revid) = find_ids(entry)
315 # First handle directory itself
316 ie = self._inventory.add_path(relpath, 'directory', id)
319 self._inventory.revision_id = revid
321 for name, entry in entries.items():
322 name = name.decode("utf-8")
326 assert isinstance(relpath, unicode)
327 assert isinstance(name, unicode)
329 subrelpath = os.path.join(relpath, name)
333 if entry.kind == svn.core.svn_node_dir:
334 subwc = svn.wc.adm_open3(wc,
335 self.workingtree.abspath(subrelpath),
338 add_dir_to_inv(subrelpath, subwc, id)
340 svn.wc.adm_close(subwc)
342 (subid, subrevid) = find_ids(entry)
343 if subid is not None:
344 add_file_to_inv(subrelpath, subid, subrevid, wc)
346 wc = workingtree._get_wc()
348 add_dir_to_inv(u"", wc, None)
352 def _abspath(self, relpath):
353 return svn.wc.get_pristine_copy_path(self.workingtree.abspath(relpath).encode("utf-8"))
355 def get_file_lines(self, file_id):
356 base_copy = self._abspath(self.id2path(file_id))
357 return osutils.split_lines(open(base_copy).read())
359 def annotate_iter(self, file_id,
360 default_revision=CURRENT_REVISION):
361 raise NotImplementedError(self.annotate_iter)