Move property definitions to a separate file.
[jelmer/subvertpy.git] / tree.py
1 # Copyright (C) 2005-2006 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 import svn.core, svn.wc, svn.delta
32 from svn.core import Pool
33
34 from bzrlib.plugins.svn import errors, properties
35
36 def parse_externals_description(base_url, val):
37     """Parse an svn:externals property value.
38
39     :param base_url: URL on which the property is set. Used for 
40         relative externals.
41
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.
45     """
46     ret = {}
47     for l in val.splitlines():
48         if l == "" or l[0] == "#":
49             continue
50         pts = l.rsplit(None, 2) 
51         if len(pts) == 3:
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]))
55         elif len(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]))
61         else:
62             raise errors.InvalidExternalsDescription()
63     return ret
64
65
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.
68     
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.
76     """
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]
81     if dir != "":
82         for part in dir.split("/"):
83             if parent.children.has_key(part):
84                 parent = parent.children[part]
85             else:
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
91
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)
97     inv.add(ie)
98
99
100 # Deal with Subversion 1.5 and the patched Subversion 1.4 (which are 
101 # slightly different).
102
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, 
108                                                    None, pool)
109
110         def wrapper(window):
111             window_handler(window, baton)
112
113         return wrapper
114 else:
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)
119
120         def wrapper(window):
121             svn.delta.invoke_txdelta_window_handler(
122                 ret[1], window, ret[2])
123
124         return wrapper
125
126
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
132         pool = Pool()
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, 
136                                                 mapping)
137         editor = TreeBuildEditor(self, pool)
138         self.file_data = {}
139         root_repos = repository.transport.get_svn_repos_root()
140         reporter = repository.transport.do_switch(
141                 self.revnum, True, 
142                 urlutils.join(root_repos, self.branch_path), editor, pool)
143         reporter.set_path("", 0, True, None, pool)
144         reporter.finish_report(pool)
145         pool.destroy()
146
147     def get_file_lines(self, file_id):
148         return osutils.split_lines(self.file_data[file_id])
149
150
151 class TreeBuildEditor(svn.delta.Editor):
152     """Builds a tree given Subversion tree transform calls."""
153     def __init__(self, tree, pool):
154         self.tree = tree
155         self.repository = tree._repository
156         self.last_revnum = {}
157         self.dir_revnum = {}
158         self.dir_ignores = {}
159         self.pool = pool
160
161     def set_target_revision(self, revnum):
162         self.revnum = revnum
163
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
169         return file_id
170
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
176         return file_id
177
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):
188             pass
189         elif name.startswith(properties.PROP_WC_PREFIX):
190             pass
191         elif name.startswith(properties.PROP_PREFIX):
192             mutter('unsupported dir property %r' % name)
193
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):
208             pass
209         elif name.startswith(properties.PROP_WC_PREFIX):
210             pass
211         elif name.startswith(properties.PROP_PREFIX):
212             mutter('unsupported file property %r' % name)
213
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
218         return path
219
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]
223
224     def close_file(self, path, checksum):
225         file_id, revision_id = self.tree.id_map[path]
226         if self.is_symlink:
227             ie = self.tree._inventory.add_path(path, 'symlink', file_id)
228         else:
229             ie = self.tree._inventory.add_path(path, 'file', file_id)
230         ie.revision = revision_id
231
232         if self.file_stream:
233             self.file_stream.seek(0)
234             file_data = self.file_stream.read()
235         else:
236             file_data = ""
237
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))
241
242         if self.is_symlink:
243             ie.symlink_target = file_data[len("link "):]
244             ie.text_sha1 = None
245             ie.text_size = None
246             ie.text_id = None
247             ie.executable = False
248         else:
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
253
254         self.file_stream = None
255
256     def close_edit(self):
257         pass
258
259     def abort_edit(self):
260         pass
261
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)
265
266
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
279
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
283                 props = props[1]
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 "):]
287                 ie.text_sha1 = None
288                 ie.text_size = None
289                 ie.text_id = None
290                 ie.executable = False
291             else:
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)
297             ie.revision = revid
298             return ie
299
300         def find_ids(entry):
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"))]
306             return (None, None)
307
308         def add_dir_to_inv(relpath, wc, parent_id):
309             entries = svn.wc.entries_read(wc, False)
310             entry = entries[""]
311             (id, revid) = find_ids(entry)
312             if id == None:
313                 return
314
315             # First handle directory itself
316             ie = self._inventory.add_path(relpath, 'directory', id)
317             ie.revision = revid
318             if relpath == u"":
319                 self._inventory.revision_id = revid
320
321             for name, entry in entries.items():
322                 name = name.decode("utf-8")
323                 if name == u"":
324                     continue
325
326                 assert isinstance(relpath, unicode)
327                 assert isinstance(name, unicode)
328
329                 subrelpath = os.path.join(relpath, name)
330
331                 assert entry
332                 
333                 if entry.kind == svn.core.svn_node_dir:
334                     subwc = svn.wc.adm_open3(wc, 
335                             self.workingtree.abspath(subrelpath), 
336                                              False, 0, None)
337                     try:
338                         add_dir_to_inv(subrelpath, subwc, id)
339                     finally:
340                         svn.wc.adm_close(subwc)
341                 else:
342                     (subid, subrevid) = find_ids(entry)
343                     if subid is not None:
344                         add_file_to_inv(subrelpath, subid, subrevid, wc)
345
346         wc = workingtree._get_wc() 
347         try:
348             add_dir_to_inv(u"", wc, None)
349         finally:
350             svn.wc.adm_close(wc)
351
352     def _abspath(self, relpath):
353         return svn.wc.get_pristine_copy_path(self.workingtree.abspath(relpath).encode("utf-8"))
354
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())
358
359     def annotate_iter(self, file_id,
360                       default_revision=CURRENT_REVISION):
361         raise NotImplementedError(self.annotate_iter)