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