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