1 # Copyright (C) 2005-2008 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 from bzrlib.plugins.svn.delta import apply_txdelta_handler
32 from bzrlib.plugins.svn import core, constants, errors, wc, properties
34 def parse_externals_description(base_url, val):
35 """Parse an svn:externals property value.
37 :param base_url: URL on which the property is set. Used for
40 :returns: dictionary with local names as keys, (revnum, url)
41 as value. revnum is the revision number and is
42 set to None if not applicable.
45 for l in val.splitlines():
46 if l == "" or l[0] == "#":
48 pts = l.rsplit(None, 2)
50 if not pts[1].startswith("-r"):
51 raise errors.InvalidExternalsDescription()
52 ret[pts[0]] = (int(pts[1][2:]), urlutils.join(base_url, pts[2]))
54 if pts[1].startswith("//"):
55 raise NotImplementedError("Relative to the scheme externals not yet supported")
56 if pts[1].startswith("^/"):
57 raise NotImplementedError("Relative to the repository root externals not yet supported")
58 ret[pts[0]] = (None, urlutils.join(base_url, pts[1]))
60 raise errors.InvalidExternalsDescription()
64 def inventory_add_external(inv, parent_id, path, revid, ref_revnum, url):
65 """Add an svn:externals entry to an inventory as a tree-reference.
67 :param inv: Inventory to add to.
68 :param parent_id: File id of directory the entry was set on.
69 :param path: Path of the entry, relative to entry with parent_id.
70 :param revid: Revision to store in newly created inventory entries.
71 :param ref_revnum: Referenced revision of tree that's being referenced, or
72 None if no specific revision is being referenced.
73 :param url: URL of referenced tree.
75 assert ref_revnum is None or isinstance(ref_revnum, int)
76 assert revid is None or isinstance(revid, str)
77 (dir, name) = os.path.split(path)
78 parent = inv[parent_id]
80 for part in dir.split("/"):
81 if parent.children.has_key(part):
82 parent = parent.children[part]
84 # Implicitly add directory if it doesn't exist yet
85 # TODO: Generate a file id
86 parent = inv.add(InventoryDirectory('someid', part,
87 parent_id=parent.file_id))
88 parent.revision = revid
90 reference_branch = Branch.open(url)
91 file_id = reference_branch.get_root_id()
92 ie = TreeReference(file_id, name, parent.file_id, revision=revid)
93 if ref_revnum is not None:
94 ie.reference_revision = reference_branch.get_rev_id(ref_revnum)
98 class SvnRevisionTree(RevisionTree):
99 """A tree that existed in a historical Subversion revision."""
100 def __init__(self, repository, revision_id):
101 self._repository = repository
102 self._revision_id = revision_id
103 (self.branch_path, self.revnum, mapping) = repository.lookup_revision_id(revision_id)
104 self._inventory = Inventory()
105 self.id_map = repository.get_fileid_map(self.revnum, self.branch_path,
107 editor = TreeBuildEditor(self)
109 root_repos = repository.transport.get_svn_repos_root()
110 conn = repository.transport.get_connection()
111 reporter = conn.do_switch(
112 self.revnum, "", True,
113 urlutils.join(root_repos, self.branch_path), editor)
115 reporter.set_path("", 0, True)
118 repository.transport.add_connection(conn)
120 def get_file_lines(self, file_id):
121 return osutils.split_lines(self.file_data[file_id])
124 class TreeBuildEditor:
125 """Builds a tree given Subversion tree transform calls."""
126 def __init__(self, tree):
128 self.repository = tree._repository
129 self.last_revnum = {}
131 def set_target_revision(self, revnum):
134 def open_root(self, revnum):
135 file_id, revision_id = self.tree.id_map[""]
136 ie = self.tree._inventory.add_path("", 'directory', file_id)
137 ie.revision = revision_id
138 self.tree._inventory.revision_id = revision_id
139 return DirectoryTreeEditor(self.tree, file_id)
147 class DirectoryTreeEditor:
148 def __init__(self, tree, file_id):
150 self.file_id = file_id
152 def add_directory(self, path, copyfrom_path=None, copyfrom_revnum=-1):
153 path = path.decode("utf-8")
154 file_id, revision_id = self.tree.id_map[path]
155 ie = self.tree._inventory.add_path(path, 'directory', file_id)
156 ie.revision = revision_id
157 return DirectoryTreeEditor(self.tree, file_id)
159 def change_prop(self, name, value):
160 if name in (properties.PROP_ENTRY_COMMITTED_DATE,
161 properties.PROP_ENTRY_COMMITTED_REV,
162 properties.PROP_ENTRY_LAST_AUTHOR,
163 properties.PROP_ENTRY_LOCK_TOKEN,
164 properties.PROP_ENTRY_UUID,
165 properties.PROP_EXECUTABLE,
166 properties.PROP_IGNORE):
168 elif name.startswith(properties.PROP_WC_PREFIX):
170 elif name.startswith(properties.PROP_PREFIX):
171 mutter('unsupported dir property %r' % name)
173 def add_file(self, path, copyfrom_path=None, copyfrom_revnum=-1):
174 path = path.decode("utf-8")
175 return FileTreeEditor(self.tree, path)
181 class FileTreeEditor:
182 def __init__(self, tree, path):
185 self.is_executable = False
186 self.is_symlink = False
187 self.last_file_rev = None
189 def change_prop(self, name, value):
190 from mapping import SVN_PROP_BZR_PREFIX
192 if name == properties.PROP_EXECUTABLE:
193 self.is_executable = (value != None)
194 elif name == properties.PROP_SPECIAL:
195 self.is_symlink = (value != None)
196 elif name == properties.PROP_EXTERNALS:
197 mutter('%r property on file!' % name)
198 elif name == properties.PROP_ENTRY_COMMITTED_REV:
199 self.last_file_rev = int(value)
200 elif name in (properties.PROP_ENTRY_COMMITTED_DATE,
201 properties.PROP_ENTRY_LAST_AUTHOR,
202 properties.PROP_ENTRY_LOCK_TOKEN,
203 properties.PROP_ENTRY_UUID,
204 properties.PROP_MIME_TYPE):
206 elif name.startswith(properties.PROP_WC_PREFIX):
208 elif name.startswith(properties.SVN_PROP_PREFIX):
209 mutter('unsupported file property %r' % name)
211 def close(self, checksum=None):
212 file_id, revision_id = self.tree.id_map[self.path]
214 ie = self.tree._inventory.add_path(self.path, 'symlink', file_id)
216 ie = self.tree._inventory.add_path(self.path, 'file', file_id)
217 ie.revision = revision_id
220 self.file_stream.seek(0)
221 file_data = self.file_stream.read()
225 actual_checksum = md5.new(file_data).hexdigest()
226 assert(checksum is None or checksum == actual_checksum,
227 "checksum mismatch: %r != %r" % (checksum, actual_checksum))
230 ie.symlink_target = file_data[len("link "):]
234 ie.executable = False
236 ie.text_sha1 = osutils.sha_string(file_data)
237 ie.text_size = len(file_data)
238 self.tree.file_data[file_id] = file_data
239 ie.executable = self.is_executable
241 self.file_stream = None
243 def apply_textdelta(self, base_checksum=None):
244 self.file_stream = StringIO()
245 return apply_txdelta_handler("", self.file_stream)
248 class SvnBasisTree(RevisionTree):
249 """Optimized version of SvnRevisionTree."""
250 def __init__(self, workingtree):
251 self.workingtree = workingtree
252 self._revision_id = workingtree.branch.generate_revision_id(
253 workingtree.base_revnum)
254 self.id_map = workingtree.branch.repository.get_fileid_map(
255 workingtree.base_revnum,
256 workingtree.branch.get_branch_path(workingtree.base_revnum),
257 workingtree.branch.mapping)
258 self._inventory = Inventory(root_id=None)
259 self._repository = workingtree.branch.repository
261 def add_file_to_inv(relpath, id, revid, adm):
262 (delta_props, props) = adm.get_prop_diffs(self.workingtree.abspath(relpath))
263 if props.has_key(properties.PROP_SPECIAL):
264 ie = self._inventory.add_path(relpath, 'symlink', id)
265 ie.symlink_target = open(self._abspath(relpath)).read()[len("link "):]
269 ie.executable = False
271 ie = self._inventory.add_path(relpath, 'file', id)
272 data = osutils.fingerprint_file(open(self._abspath(relpath)))
273 ie.text_sha1 = data['sha1']
274 ie.text_size = data['size']
275 ie.executable = props.has_key(properties.PROP_EXECUTABLE)
280 relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
281 if entry.schedule in (wc.SCHEDULE_NORMAL,
283 wc.SCHEDULE_REPLACE):
284 return self.id_map[workingtree.branch.unprefix(relpath.decode("utf-8"))]
287 def add_dir_to_inv(relpath, adm, parent_id):
288 entries = adm.entries_read(False)
290 (id, revid) = find_ids(entry)
294 # First handle directory itself
295 ie = self._inventory.add_path(relpath, 'directory', id)
298 self._inventory.revision_id = revid
300 for name, entry in entries.items():
301 name = name.decode("utf-8")
305 assert isinstance(relpath, unicode)
306 assert isinstance(name, unicode)
308 subrelpath = os.path.join(relpath, name)
312 if entry.kind == core.NODE_DIR:
313 subwc = wc.WorkingCopy(adm,
314 self.workingtree.abspath(subrelpath),
317 add_dir_to_inv(subrelpath, subwc, id)
321 (subid, subrevid) = find_ids(entry)
322 if subid is not None:
323 add_file_to_inv(subrelpath, subid, subrevid, adm)
325 adm = workingtree._get_wc()
327 add_dir_to_inv(u"", adm, None)
331 def _abspath(self, relpath):
332 return wc.get_pristine_copy_path(self.workingtree.abspath(relpath).encode("utf-8"))
334 def get_file_lines(self, file_id):
335 base_copy = self._abspath(self.id2path(file_id))
336 return osutils.split_lines(open(base_copy).read())
338 def annotate_iter(self, file_id,
339 default_revision=CURRENT_REVISION):
340 raise NotImplementedError(self.annotate_iter)