Merge property changes from 0.4.
[jelmer/subvertpy.git] / tree.py
1 # Copyright (C) 2005-2008 Jelmer Vernooij <jelmer@samba.org>
2
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.
7
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.
12
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."""
17
18 from bzrlib import osutils, urlutils
19 from bzrlib.branch import Branch
20 from bzrlib.inventory import Inventory, InventoryDirectory, TreeReference
21
22 from bzrlib.revision import CURRENT_REVISION
23 from bzrlib.trace import mutter
24 from bzrlib.revisiontree import RevisionTree
25
26 import os
27 import md5
28 from cStringIO import StringIO
29 import urllib
30
31 from bzrlib.plugins.svn.delta import apply_txdelta_handler
32 from bzrlib.plugins.svn import core, constants, errors, wc, properties
33
34 def parse_externals_description(base_url, val):
35     """Parse an svn:externals property value.
36
37     :param base_url: URL on which the property is set. Used for 
38         relative externals.
39
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.
43     """
44     ret = {}
45     for l in val.splitlines():
46         if l == "" or l[0] == "#":
47             continue
48         pts = l.rsplit(None, 2) 
49         if len(pts) == 3:
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]))
53         elif len(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]))
59         else:
60             raise errors.InvalidExternalsDescription()
61     return ret
62
63
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.
66     
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.
74     """
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]
79     if dir != "":
80         for part in dir.split("/"):
81             if parent.children.has_key(part):
82                 parent = parent.children[part]
83             else:
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
89
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)
95     inv.add(ie)
96
97
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, 
106                                                 mapping)
107         editor = TreeBuildEditor(self)
108         self.file_data = {}
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)
114         try:
115             reporter.set_path("", 0, True)
116             reporter.finish()
117         finally:
118             repository.transport.add_connection(conn)
119
120     def get_file_lines(self, file_id):
121         return osutils.split_lines(self.file_data[file_id])
122
123
124 class TreeBuildEditor:
125     """Builds a tree given Subversion tree transform calls."""
126     def __init__(self, tree):
127         self.tree = tree
128         self.repository = tree._repository
129         self.last_revnum = {}
130
131     def set_target_revision(self, revnum):
132         self.revnum = revnum
133
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)
140
141     def close(self):
142         pass
143
144     def abort(self):
145         pass
146
147 class DirectoryTreeEditor:
148     def __init__(self, tree, file_id):
149         self.tree = tree
150         self.file_id = file_id
151
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)
158
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):
167             pass
168         elif name.startswith(properties.PROP_WC_PREFIX):
169             pass
170         elif name.startswith(properties.PROP_PREFIX):
171             mutter('unsupported dir property %r' % name)
172
173     def add_file(self, path, copyfrom_path=None, copyfrom_revnum=-1):
174         path = path.decode("utf-8")
175         return FileTreeEditor(self.tree, path)
176
177     def close(self):
178         pass
179
180
181 class FileTreeEditor:
182     def __init__(self, tree, path):
183         self.tree = tree
184         self.path = path
185         self.is_executable = False
186         self.is_symlink = False
187         self.last_file_rev = None
188
189     def change_prop(self, name, value):
190         from mapping import SVN_PROP_BZR_PREFIX
191
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):
205             pass
206         elif name.startswith(properties.PROP_WC_PREFIX):
207             pass
208         elif name.startswith(properties.SVN_PROP_PREFIX):
209             mutter('unsupported file property %r' % name)
210
211     def close(self, checksum=None):
212         file_id, revision_id = self.tree.id_map[self.path]
213         if self.is_symlink:
214             ie = self.tree._inventory.add_path(self.path, 'symlink', file_id)
215         else:
216             ie = self.tree._inventory.add_path(self.path, 'file', file_id)
217         ie.revision = revision_id
218
219         if self.file_stream:
220             self.file_stream.seek(0)
221             file_data = self.file_stream.read()
222         else:
223             file_data = ""
224
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))
228
229         if self.is_symlink:
230             ie.symlink_target = file_data[len("link "):]
231             ie.text_sha1 = None
232             ie.text_size = None
233             ie.text_id = None
234             ie.executable = False
235         else:
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
240
241         self.file_stream = None
242
243     def apply_textdelta(self, base_checksum=None):
244         self.file_stream = StringIO()
245         return apply_txdelta_handler("", self.file_stream)
246
247
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
260
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 "):]
266                 ie.text_sha1 = None
267                 ie.text_size = None
268                 ie.text_id = None
269                 ie.executable = False
270             else:
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)
276             ie.revision = revid
277             return ie
278
279         def find_ids(entry):
280             relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
281             if entry.schedule in (wc.SCHEDULE_NORMAL, 
282                                   wc.SCHEDULE_DELETE, 
283                                   wc.SCHEDULE_REPLACE):
284                 return self.id_map[workingtree.branch.unprefix(relpath.decode("utf-8"))]
285             return (None, None)
286
287         def add_dir_to_inv(relpath, adm, parent_id):
288             entries = adm.entries_read(False)
289             entry = entries[""]
290             (id, revid) = find_ids(entry)
291             if id == None:
292                 return
293
294             # First handle directory itself
295             ie = self._inventory.add_path(relpath, 'directory', id)
296             ie.revision = revid
297             if relpath == u"":
298                 self._inventory.revision_id = revid
299
300             for name, entry in entries.items():
301                 name = name.decode("utf-8")
302                 if name == u"":
303                     continue
304
305                 assert isinstance(relpath, unicode)
306                 assert isinstance(name, unicode)
307
308                 subrelpath = os.path.join(relpath, name)
309
310                 assert entry
311                 
312                 if entry.kind == core.NODE_DIR:
313                     subwc = wc.WorkingCopy(adm, 
314                             self.workingtree.abspath(subrelpath), 
315                                              False, 0, None)
316                     try:
317                         add_dir_to_inv(subrelpath, subwc, id)
318                     finally:
319                         subwc.close()
320                 else:
321                     (subid, subrevid) = find_ids(entry)
322                     if subid is not None:
323                         add_file_to_inv(subrelpath, subid, subrevid, adm)
324
325         adm = workingtree._get_wc() 
326         try:
327             add_dir_to_inv(u"", adm, None)
328         finally:
329             adm.close()
330
331     def _abspath(self, relpath):
332         return wc.get_pristine_copy_path(self.workingtree.abspath(relpath).encode("utf-8"))
333
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())
337
338     def annotate_iter(self, file_id,
339                       default_revision=CURRENT_REVISION):
340         raise NotImplementedError(self.annotate_iter)