Merge 0.4 branch.
[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 2 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.inventory import Inventory
19
20 from bzrlib import osutils, urlutils
21 from bzrlib.trace import mutter
22 from bzrlib.revisiontree import RevisionTree
23
24 import os
25 import md5
26 from cStringIO import StringIO
27 import urllib
28
29 import svn.core, svn.wc, svn.delta
30 from svn.core import Pool
31
32 # Deal with Subversion 1.5 and the patched Subversion 1.4 (which are 
33 # slightly different).
34
35 if hasattr(svn.delta, 'tx_invoke_window_handler'):
36     def apply_txdelta_handler(src_stream, target_stream, pool):
37         assert hasattr(src_stream, 'read')
38         assert hasattr(target_stream, 'write')
39         window_handler, baton = svn.delta.tx_apply(src_stream, target_stream, 
40                                                    None, pool)
41
42         def wrapper(window):
43             window_handler(window, baton)
44
45         return wrapper
46 else:
47     def apply_txdelta_handler(src_stream, target_stream, pool):
48         assert hasattr(src_stream, 'read')
49         assert hasattr(target_stream, 'write')
50         ret = svn.delta.svn_txdelta_apply(src_stream, target_stream, None, pool)
51
52         def wrapper(window):
53             svn.delta.invoke_txdelta_window_handler(
54                 ret[1], window, ret[2])
55
56         return wrapper
57
58 class SvnRevisionTree(RevisionTree):
59     """A tree that existed in a historical Subversion revision."""
60     def __init__(self, repository, revision_id):
61         self._repository = repository
62         self._revision_id = revision_id
63         pool = Pool()
64         (self.branch_path, self.revnum, scheme) = repository.lookup_revision_id(revision_id)
65         self._inventory = Inventory()
66         self.id_map = repository.get_fileid_map(self.revnum, self.branch_path, 
67                                                 scheme)
68         self.editor = TreeBuildEditor(self, pool)
69         self.file_data = {}
70         editor, baton = svn.delta.make_editor(self.editor, pool)
71         root_repos = repository.transport.get_svn_repos_root()
72         reporter = repository.transport.do_switch(
73                 self.revnum, True, 
74                 urlutils.join(root_repos, self.branch_path), editor, baton, pool)
75         reporter.set_path("", 0, True, None, pool)
76         reporter.finish_report(pool)
77         pool.destroy()
78
79     def get_file_lines(self, file_id):
80         return osutils.split_lines(self.file_data[file_id])
81
82
83 class TreeBuildEditor(svn.delta.Editor):
84     """Builds a tree given Subversion tree transform calls."""
85     def __init__(self, tree, pool):
86         self.tree = tree
87         self.repository = tree._repository
88         self.last_revnum = {}
89         self.dir_revnum = {}
90         self.dir_ignores = {}
91         self.pool = pool
92
93     def set_target_revision(self, revnum):
94         self.revnum = revnum
95
96     def open_root(self, revnum, baton):
97         file_id, revision_id = self.tree.id_map[""]
98         ie = self.tree._inventory.add_path("", 'directory', file_id)
99         ie.revision = revision_id
100         self.tree._inventory.revision_id = revision_id
101         return file_id
102
103     def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
104         path = path.decode("utf-8")
105         file_id, revision_id = self.tree.id_map[path]
106         ie = self.tree._inventory.add_path(path, 'directory', file_id)
107         ie.revision = revision_id
108         return file_id
109
110     def change_dir_prop(self, id, name, value, pool):
111         from repository import (SVN_PROP_BZR_ANCESTRY, 
112                         SVN_PROP_BZR_PREFIX, SVN_PROP_BZR_REVISION_INFO, 
113                         SVN_PROP_BZR_FILEIDS, SVN_PROP_BZR_REVISION_ID,
114                         SVN_PROP_BZR_BRANCHING_SCHEME, SVN_PROP_BZR_MERGE)
115
116         if name == svn.core.SVN_PROP_ENTRY_COMMITTED_REV:
117             self.dir_revnum[id] = int(value)
118         elif name == svn.core.SVN_PROP_IGNORE:
119             self.dir_ignores[id] = value
120         elif name.startswith(SVN_PROP_BZR_ANCESTRY):
121             if id != self.tree._inventory.root.file_id:
122                 mutter('%r set on non-root dir!' % name)
123                 return
124         elif name in (SVN_PROP_BZR_FILEIDS, SVN_PROP_BZR_BRANCHING_SCHEME):
125             if id != self.tree._inventory.root.file_id:
126                 mutter('%r set on non-root dir!' % name)
127                 return
128         elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
129                       svn.core.SVN_PROP_ENTRY_LAST_AUTHOR,
130                       svn.core.SVN_PROP_ENTRY_LOCK_TOKEN,
131                       svn.core.SVN_PROP_ENTRY_UUID,
132                       svn.core.SVN_PROP_EXECUTABLE):
133             pass
134         elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
135             pass
136         elif (name == SVN_PROP_BZR_REVISION_INFO or 
137               name.startswith(SVN_PROP_BZR_REVISION_ID)):
138             pass
139         elif name == SVN_PROP_BZR_MERGE:
140             pass
141         elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
142               name.startswith(SVN_PROP_BZR_PREFIX)):
143             mutter('unsupported dir property %r' % name)
144
145     def change_file_prop(self, id, name, value, pool):
146         from repository import SVN_PROP_BZR_PREFIX
147
148         if name == svn.core.SVN_PROP_EXECUTABLE:
149             self.is_executable = (value != None)
150         elif name == svn.core.SVN_PROP_SPECIAL:
151             self.is_symlink = (value != None)
152         elif name == svn.core.SVN_PROP_ENTRY_COMMITTED_REV:
153             self.last_file_rev = int(value)
154         elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
155                       svn.core.SVN_PROP_ENTRY_LAST_AUTHOR,
156                       svn.core.SVN_PROP_ENTRY_LOCK_TOKEN,
157                       svn.core.SVN_PROP_ENTRY_UUID,
158                       svn.core.SVN_PROP_MIME_TYPE):
159             pass
160         elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
161             pass
162         elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
163               name.startswith(SVN_PROP_BZR_PREFIX)):
164             mutter('unsupported file property %r' % name)
165
166     def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
167         path = path.decode("utf-8")
168         self.is_symlink = False
169         self.is_executable = False
170         return path
171
172     def close_dir(self, id):
173         if id in self.tree._inventory and self.dir_ignores.has_key(id):
174             self.tree._inventory[id].ignores = self.dir_ignores[id]
175
176     def close_file(self, path, checksum):
177         file_id, revision_id = self.tree.id_map[path]
178         if self.is_symlink:
179             ie = self.tree._inventory.add_path(path, 'symlink', file_id)
180         else:
181             ie = self.tree._inventory.add_path(path, 'file', file_id)
182         ie.revision = revision_id
183
184         if self.file_stream:
185             self.file_stream.seek(0)
186             file_data = self.file_stream.read()
187         else:
188             file_data = ""
189
190         actual_checksum = md5.new(file_data).hexdigest()
191         assert(checksum is None or checksum == actual_checksum,
192                 "checksum mismatch: %r != %r" % (checksum, actual_checksum))
193
194         if self.is_symlink:
195             ie.symlink_target = file_data[len("link "):]
196             ie.text_sha1 = None
197             ie.text_size = None
198             ie.text_id = None
199             ie.executable = False
200         else:
201             ie.text_sha1 = osutils.sha_string(file_data)
202             ie.text_size = len(file_data)
203             self.tree.file_data[file_id] = file_data
204             ie.executable = self.is_executable
205
206         self.file_stream = None
207
208     def close_edit(self):
209         pass
210
211     def abort_edit(self):
212         pass
213
214     def apply_textdelta(self, file_id, base_checksum):
215         self.file_stream = StringIO()
216         return apply_txdelta_handler(StringIO(""), self.file_stream, self.pool)
217
218
219 class SvnBasisTree(RevisionTree):
220     """Optimized version of SvnRevisionTree."""
221     def __init__(self, workingtree):
222         self.workingtree = workingtree
223         self._revision_id = workingtree.branch.generate_revision_id(
224                                       workingtree.base_revnum)
225         self.id_map = workingtree.branch.repository.get_fileid_map(
226                 workingtree.base_revnum, 
227                 workingtree.branch.get_branch_path(workingtree.base_revnum), 
228                 workingtree.branch.scheme)
229         self._inventory = Inventory(root_id=None)
230         self._repository = workingtree.branch.repository
231
232         def add_file_to_inv(relpath, id, revid, wc):
233             props = svn.wc.get_prop_diffs(self.workingtree.abspath(relpath), wc)
234             if isinstance(props, list): # Subversion 1.5
235                 props = props[1]
236             if props.has_key(svn.core.SVN_PROP_SPECIAL):
237                 ie = self._inventory.add_path(relpath, 'symlink', id)
238                 ie.symlink_target = open(self._abspath(relpath)).read()[len("link "):]
239                 ie.text_sha1 = None
240                 ie.text_size = None
241                 ie.text_id = None
242                 ie.executable = False
243             else:
244                 ie = self._inventory.add_path(relpath, 'file', id)
245                 data = osutils.fingerprint_file(open(self._abspath(relpath)))
246                 ie.text_sha1 = data['sha1']
247                 ie.text_size = data['size']
248                 ie.executable = props.has_key(svn.core.SVN_PROP_EXECUTABLE)
249             ie.revision = revid
250             return ie
251
252         def find_ids(entry):
253             relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
254             if entry.schedule in (svn.wc.schedule_normal, 
255                                   svn.wc.schedule_delete, 
256                                   svn.wc.schedule_replace):
257                 return self.id_map[workingtree.branch.scheme.unprefix(relpath)[1]]
258             return (None, None)
259
260         def add_dir_to_inv(relpath, wc, parent_id):
261             entries = svn.wc.entries_read(wc, False)
262             entry = entries[""]
263             (id, revid) = find_ids(entry)
264             if id == None:
265                 return
266
267             # First handle directory itself
268             ie = self._inventory.add_path(relpath, 'directory', id)
269             ie.revision = revid
270             if relpath == "":
271                 self._inventory.revision_id = revid
272
273             for name in entries:
274                 if name == "":
275                     continue
276
277                 subrelpath = os.path.join(relpath, name)
278
279                 entry = entries[name]
280                 assert entry
281                 
282                 if entry.kind == svn.core.svn_node_dir:
283                     subwc = svn.wc.adm_open3(wc, 
284                             self.workingtree.abspath(subrelpath), 
285                                              False, 0, None)
286                     try:
287                         add_dir_to_inv(subrelpath, subwc, id)
288                     finally:
289                         svn.wc.adm_close(subwc)
290                 else:
291                     (subid, subrevid) = find_ids(entry)
292                     if subid is not None:
293                         add_file_to_inv(subrelpath, subid, subrevid, wc)
294
295         wc = workingtree._get_wc() 
296         try:
297             add_dir_to_inv("", wc, None)
298         finally:
299             svn.wc.adm_close(wc)
300
301     def _abspath(self, relpath):
302         return svn.wc.get_pristine_copy_path(self.workingtree.abspath(relpath))
303
304     def get_file_lines(self, file_id):
305         base_copy = self._abspath(self.id2path(file_id))
306         return osutils.split_lines(open(base_copy).read())
307