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