]> git.samba.org - jelmer/subvertpy.git/blob - branch.py
Start moving functionality to SvnRepository
[jelmer/subvertpy.git] / branch.py
1 # Foreign branch support for Subversion
2 # Copyright (C) 2005 Jelmer Vernooij <jelmer@samba.org>
3 #
4 # Published under the GNU GPL
5
6 """Branch support for Subversion repositories
7
8 Support for SVN branches has been splitted up into two kinds: 
9 - RA (remote access) Subversion URLs such as svn+ssh://..., 
10     http:// (webdav) or file:/// 
11 - wc (working copy) local checkouts. These are directories that contain a 
12     .svn/ subdirectory)
13
14 Subversion always relies on the repository for the history information. Thus,
15 RA can roughly be mapped to what bzr calls a Branch, and wc to what bzr calls a 
16 WorkingTree.
17
18 Three different identifiers are used in this file to refer to 
19 revisions:
20 - revid: bzr revision ids (text data, usually containing email 
21     address + sha)
22 - revno: bzr revision number
23 - revnum: svn revision number
24 """
25
26 from bzrlib.branch import Branch
27 from bzrlib.errors import NotBranchError,NoWorkingTree,NoSuchRevision
28 from bzrlib.inventory import Inventory, InventoryFile, InventoryDirectory, \
29             ROOT_ID
30 from bzrlib.revision import Revision, NULL_REVISION
31 from bzrlib.tree import Tree, EmptyTree
32 from bzrlib.trace import mutter, note
33 from bzrlib.workingtree import WorkingTree
34 import bzrlib
35
36 import svn.core, svn.client, svn.wc
37 import os
38 from libsvn._core import SubversionException
39
40 # Initialize APR (required for all SVN calls)
41 svn.core.apr_initialize()
42
43 global_pool = svn.core.svn_pool_create(None)
44
45 def _create_auth_baton(pool):
46     # Give the client context baton a suite of authentication
47     # providers.
48     providers = [
49         svn.client.svn_client_get_simple_provider(pool),
50         svn.client.svn_client_get_ssl_client_cert_file_provider(pool),
51         svn.client.svn_client_get_ssl_client_cert_pw_file_provider(pool),
52         svn.client.svn_client_get_ssl_server_trust_file_provider(pool),
53         svn.client.svn_client_get_username_provider(pool),
54         ]
55     return svn.core.svn_auth_open(providers, pool)
56
57 auth_baton = _create_auth_baton(global_pool)
58
59 class SvnRevisionTree(Tree):
60     def __init__(self,branch,revision_id):
61         self.branch = branch
62         self.revision_id = revision_id
63         self.revnum = self.branch.get_revnum(revision_id)
64         self._inventory = branch.get_inventory(revision_id)
65
66     def get_file_sha1(self,file_id):
67         return bzrlib.osutils.sha_string(self.get_file(file_id))
68
69     def is_executable(self,file_id):
70         filename = self.branch.url_from_file_id(self.revision_id,file_id)
71         mutter("svn propget %r %r" % (svn.core.SVN_PROP_EXECUTABLE, filename))
72         values = svn.client.propget(svn.core.SVN_PROP_EXECUTABLE, filename, self.revnum, False, self.repository.client, self.repository.pool)
73         if len(values) == 1 and values.pop() == svn.core.SVN_PROP_EXECUTABLE_VALUE:
74             return True
75         return False 
76     
77     def get_file(self,file_id):
78         stream = svn.core.svn_stream_empty(self.repository.pool)
79         url = self.branch.url_from_file_id(self.revision_id,file_id)
80         mutter("svn cat -r %r %r" % (self.revnum.value.number,url))
81         svn.repository.client.cat(stream,url.encode('utf8'),self.revnum,self.repository.client,self.repository.pool)
82         return Stream(stream).read()
83
84 class SvnBranch(Branch):
85     def __init__(self,repos,base,kind):
86         self.repository = repos
87         self.base = base 
88         self._get_last_revnum(kind)
89         
90     #FIXME
91     def filename_from_file_id(self,revision_id,file_id):
92         """Generate a Subversion filename from a bzr file id."""
93         return file_id.replace('_','/')
94
95     def filename_to_file_id(self,revision_id,filename):
96         """Generate a bzr file id from a Subversion file name."""
97         return filename.replace('/','_')
98
99     def url_from_file_id(self,revision_id,file_id):
100         """Generate a full Subversion URL from a bzr file id."""
101         return self.base+"/"+self.filename_from_file_id(revision_id,file_id)
102
103     def _get_last_revnum(self,kind):
104         # The python bindings for the svn_client_info() function
105         # are broken, so this is the only way to (cheaply) find out what the 
106         # youngest revision number is
107         revt_head = svn.core.svn_opt_revision_t()
108         revt_head.kind = kind
109         self.last_revnum = None
110         def rcvr(paths,rev,author,date,message,pool):
111             self.last_revnum = rev
112         mutter("svn log -r HEAD %r" % self.base)
113         svn.client.log3([self.base.encode('utf8')], revt_head, revt_head, \
114                 revt_head, 1, False, False, rcvr, 
115                 self.repository.client, self.repository.pool)
116         assert self.last_revnum
117
118     def _generate_revnum_map(self):
119         #FIXME: Revids should be globally unique, so we should include the 
120         # branch path somehow. If we don't do this there might be revisions 
121         # that have the same id because they were created in the same commit.
122         # This requires finding out the URL of the root of the repository, 
123         # but this is not possible at the moment since svn.client.info() does
124         # not work.
125         self.revid_map = {}
126         self.revnum_map = {}
127         self._revision_history = []
128         for revnum in range(0,self.last_revnum+1):
129             revt = svn.core.svn_opt_revision_t()
130             revt.kind = svn.core.svn_opt_revision_number
131             revt.value.number = revnum
132             if revnum == 0:
133                 revid = None
134             else:
135                 revid = "%d@%s" % (revnum,self.repository.uuid)
136                 self._revision_history.append(revid)
137             self.revid_map[revid] = revt
138             self.revnum_map[revnum] = revid
139
140     def get_revnum(self,revid):
141         """Map bzr revision id to a SVN revision number."""
142         try:
143             return self.revid_map[revid]
144         except KeyError:
145             raise NoSuchRevision(revid,self)
146
147     def set_root_id(self, file_id):
148         raise NotImplementedError('set_root_id not supported on Subversion Branches')
149             
150     def get_root_id(self):
151         inv = self.get_inventory(self.last_revision())
152         return inv.root.file_id
153
154     def abspath(self, name):
155         return self.base+"/"+name
156
157     def push_stores(self, branch_to):
158         raise NotImplementedError('push_stores is abstract') #FIXME
159
160     def get_revision_inventory(self, revision_id):
161         raise NotImplementedError('get_revision_inventory is abstract') #FIXME
162
163     def sign_revision(self, revision_id, gpg_strategy):
164         raise NotImplementedError('Subversion revisions can not be signed')
165
166     def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
167         raise NotImplementedError('Subversion revisions can not be signed')
168
169     def set_revision_history(self, rev_history):
170         raise NotImplementedError('set_revision_history not supported on Subversion branches')
171
172     def set_push_location(self, location):
173         raise NotImplementedError('set_push_location not supported on Subversion')
174
175     def get_push_location(self):
176         raise NotImplementedError('get_push_location not supported on Subversion')
177
178     def revision_history(self):
179         return self._revision_history
180
181     def has_revision(self, revision_id):
182         return self.revid_map.has_key(revision_id)
183
184     def print_file(self, file, revno):
185         """See Branch.print_file."""
186         # For some odd reason this method still takes a revno rather 
187         # then a revid
188         revnum = self.get_revnum(self.get_rev_id(revno))
189         stream = svn.core.svn_stream_empty(self.repository.pool)
190         file_url = self.base+"/"+file
191         mutter('svn cat -r %r %r' % (revnum.value.number,file_url))
192         svn.client.cat(stream,file_url.encode('utf8'),revnum,self.client,self.repository.pool)
193         print Stream(stream).read()
194
195     def get_revision(self, revision_id):
196         revnum = self.get_revnum(revision_id)
197         
198         mutter('svn proplist -r %r %r' % (revnum.value.number,self.base))
199         (svn_props, actual_rev) = svn.client.revprop_list(self.base.encode('utf8'), revnum, self.repository.client, self.repository.pool)
200         assert actual_rev == revnum.value.number
201
202         parent_ids = self.get_parents(revision_id)
203     
204         # Commit SVN revision properties to a Revision object
205         bzr_props = {}
206         rev = Revision(revision_id=revision_id,
207                        parent_ids=parent_ids)
208
209         for name in svn_props:
210             bzr_props[name] = str(svn_props[name])
211
212         rev.timestamp = svn.core.secs_from_timestr(bzr_props[svn.core.SVN_PROP_REVISION_DATE], self.repository.pool) * 1.0
213         rev.timezone = None
214
215         rev.committer = bzr_props[svn.core.SVN_PROP_REVISION_AUTHOR]
216         rev.message = bzr_props[svn.core.SVN_PROP_REVISION_LOG]
217
218         rev.properties = bzr_props
219         rev.inventory_sha1 = self.get_inventory_sha1(revision_id)
220         
221         return rev
222
223     def get_parents(self, revision_id):
224         revnum = self.get_revnum(revision_id)
225         parents = []
226         if not revision_id is None:
227             parent_id = self.revnum_map[revnum.value.number-1]
228             if not parent_id is None:
229                 parents.append(parent_id)
230         # FIXME: Figure out if there are any merges here and where they come 
231         # from
232         return parents
233
234     def get_ancestry(self, revision_id):
235         try:
236             i = self.revision_history().index(revision_id)
237         except ValueError:
238             raise NoSuchRevision(revision_id,self)
239
240         # FIXME: Figure out if there are any merges here and where they come 
241         # from
242         return self.revision_history()[0:i+1]
243
244     def get_inventory(self, revision_id):
245         revnum = self.get_revnum(revision_id)
246         mutter('getting inventory %r for branch %r' % (revnum.value.number, self.base))
247
248         mutter("svn ls -r %d '%r'" % (revnum.value.number, self.base))
249         remote_ls = svn.client.ls(self.base.encode('utf8'),
250                                          revnum,
251                                          True, # recurse
252                                          self.repository.client, 
253                                          self.repository.pool)
254         mutter('done')
255
256         # Make sure a directory is always added before its contents
257         names = remote_ls.keys()
258         names.sort(lambda a,b: len(a) - len(b))
259
260         inv = Inventory()
261         for entry in names:
262             ri = entry.rfind('/')
263             if ri == -1:
264                 top = entry
265                 parent = ''
266             else:
267                 top = entry[ri+1:]
268                 parent = entry[0:ri]
269
270             parent_id = inv.path2id(parent)
271             assert not parent_id is None
272             
273             id = self.filename_to_file_id(revision_id, entry)
274
275             if remote_ls[entry].kind == svn.core.svn_node_dir:
276                 inv.add(InventoryDirectory(id,top,parent_id=parent_id))
277             elif remote_ls[entry].kind == svn.core.svn_node_file:
278                 inv.add(InventoryFile(id,top,parent_id=parent_id))
279             else:
280                 raise BzrError("Unknown entry kind for '%s': %d" % (entry, remote_ls[entry].kind))
281
282         return inv
283
284     def pull(self, source, overwrite=False):
285         print "Pull from %s to %s" % (source,self)
286         raise NotImplementedError('pull is abstract') #FIXME
287
288     def update_revisions(self, other, stop_revision=None):
289         raise NotImplementedError('update_revisions is abstract') #FIXME
290
291     def pullable_revisions(self, other, stop_revision):
292         raise NotImplementedError('pullable_revisions is abstract') #FIXME
293         
294     def revision_tree(self, revision_id):
295         if revision_id is None or revision_id == NULL_REVISION:
296             return EmptyTree()
297         
298         return SvnRevisionTree(self, revision_id)
299
300     # The remote server handles all this for us
301     def lock_write(self):
302         pass
303         
304     def lock_read(self):
305         pass
306
307     def unlock(self):
308         pass
309
310     def get_parent(self):
311         return None
312
313     def set_parent(self, url):
314         raise NotImplementedError('can not change parent of SVN branch')
315
316     def get_transaction(self):
317         raise NotImplementedError('get_transaction is abstract') #FIXME
318
319     def append_revision(self, *revision_ids):
320         raise NotImplementedError('append_revision is abstract') #FIXME
321
322     def working_tree(self):
323         if self.path is None:
324             raise NoWorkingTree(self.base)
325         else:
326             return SvnWorkingTree(self.path,branch=self)
327
328     # FIXME: perhaps move these four to a 'ForeignBranch' class in 
329     # bzr core?
330     def get_revision_xml(self, revision_id):
331         return bzrlib.xml5.serializer_v5.write_revision_to_string(self.get_revision(revision_id))
332
333     def get_inventory_xml(self, revision_id):
334         return bzrlib.xml5.serializer_v5.write_inventory_to_string(self.get_inventory(revision_id))
335
336     def get_revision_sha1(self, revision_id):
337         return bzrlib.osutils.sha_string(self.get_revision_xml(revision_id))
338
339     def get_inventory_sha1(self, revision_id):
340         return bzrlib.osutils.sha_string(self.get_inventory_xml(revision_id))