1 # Foreign branch support for Subversion
2 # Copyright (C) 2005 Jelmer Vernooij <jelmer@samba.org>
4 # Published under the GNU GPL
6 """Branch support for Subversion repositories
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
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
18 Three different identifiers are used in this file to refer to
20 - revid: bzr revision ids (text data, usually containing email
22 - revno: bzr revision number
23 - revnum: svn revision number
26 from bzrlib.branch import Branch
27 from bzrlib.errors import NotBranchError,NoWorkingTree,NoSuchRevision
28 from bzrlib.inventory import Inventory, InventoryFile, InventoryDirectory, \
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
36 import svn.core, svn.client, svn.wc
38 from libsvn._core import SubversionException
40 # Initialize APR (required for all SVN calls)
41 svn.core.apr_initialize()
43 global_pool = svn.core.svn_pool_create(None)
45 def _create_auth_baton(pool):
46 # Give the client context baton a suite of authentication
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),
55 return svn.core.svn_auth_open(providers, pool)
57 auth_baton = _create_auth_baton(global_pool)
59 class SvnRevisionTree(Tree):
60 def __init__(self,branch,revision_id):
62 self.revision_id = revision_id
63 self.revnum = self.branch.get_revnum(revision_id)
64 self._inventory = branch.get_inventory(revision_id)
66 def get_file_sha1(self,file_id):
67 return bzrlib.osutils.sha_string(self.get_file(file_id))
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:
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()
84 class SvnBranch(Branch):
85 def __init__(self,repos,base,kind):
86 self.repository = repos
88 self._get_last_revnum(kind)
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('_','/')
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('/','_')
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)
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
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
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
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
140 def get_revnum(self,revid):
141 """Map bzr revision id to a SVN revision number."""
143 return self.revid_map[revid]
145 raise NoSuchRevision(revid,self)
147 def set_root_id(self, file_id):
148 raise NotImplementedError('set_root_id not supported on Subversion Branches')
150 def get_root_id(self):
151 inv = self.get_inventory(self.last_revision())
152 return inv.root.file_id
154 def abspath(self, name):
155 return self.base+"/"+name
157 def push_stores(self, branch_to):
158 raise NotImplementedError('push_stores is abstract') #FIXME
160 def get_revision_inventory(self, revision_id):
161 raise NotImplementedError('get_revision_inventory is abstract') #FIXME
163 def sign_revision(self, revision_id, gpg_strategy):
164 raise NotImplementedError('Subversion revisions can not be signed')
166 def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
167 raise NotImplementedError('Subversion revisions can not be signed')
169 def set_revision_history(self, rev_history):
170 raise NotImplementedError('set_revision_history not supported on Subversion branches')
172 def set_push_location(self, location):
173 raise NotImplementedError('set_push_location not supported on Subversion')
175 def get_push_location(self):
176 raise NotImplementedError('get_push_location not supported on Subversion')
178 def revision_history(self):
179 return self._revision_history
181 def has_revision(self, revision_id):
182 return self.revid_map.has_key(revision_id)
184 def print_file(self, file, revno):
185 """See Branch.print_file."""
186 # For some odd reason this method still takes a revno rather
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()
195 def get_revision(self, revision_id):
196 revnum = self.get_revnum(revision_id)
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
202 parent_ids = self.get_parents(revision_id)
204 # Commit SVN revision properties to a Revision object
206 rev = Revision(revision_id=revision_id,
207 parent_ids=parent_ids)
209 for name in svn_props:
210 bzr_props[name] = str(svn_props[name])
212 rev.timestamp = svn.core.secs_from_timestr(bzr_props[svn.core.SVN_PROP_REVISION_DATE], self.repository.pool) * 1.0
215 rev.committer = bzr_props[svn.core.SVN_PROP_REVISION_AUTHOR]
216 rev.message = bzr_props[svn.core.SVN_PROP_REVISION_LOG]
218 rev.properties = bzr_props
219 rev.inventory_sha1 = self.get_inventory_sha1(revision_id)
223 def get_parents(self, revision_id):
224 revnum = self.get_revnum(revision_id)
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
234 def get_ancestry(self, revision_id):
236 i = self.revision_history().index(revision_id)
238 raise NoSuchRevision(revision_id,self)
240 # FIXME: Figure out if there are any merges here and where they come
242 return self.revision_history()[0:i+1]
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))
248 mutter("svn ls -r %d '%r'" % (revnum.value.number, self.base))
249 remote_ls = svn.client.ls(self.base.encode('utf8'),
252 self.repository.client,
253 self.repository.pool)
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))
262 ri = entry.rfind('/')
270 parent_id = inv.path2id(parent)
271 assert not parent_id is None
273 id = self.filename_to_file_id(revision_id, entry)
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))
280 raise BzrError("Unknown entry kind for '%s': %d" % (entry, remote_ls[entry].kind))
284 def pull(self, source, overwrite=False):
285 print "Pull from %s to %s" % (source,self)
286 raise NotImplementedError('pull is abstract') #FIXME
288 def update_revisions(self, other, stop_revision=None):
289 raise NotImplementedError('update_revisions is abstract') #FIXME
291 def pullable_revisions(self, other, stop_revision):
292 raise NotImplementedError('pullable_revisions is abstract') #FIXME
294 def revision_tree(self, revision_id):
295 if revision_id is None or revision_id == NULL_REVISION:
298 return SvnRevisionTree(self, revision_id)
300 # The remote server handles all this for us
301 def lock_write(self):
310 def get_parent(self):
313 def set_parent(self, url):
314 raise NotImplementedError('can not change parent of SVN branch')
316 def get_transaction(self):
317 raise NotImplementedError('get_transaction is abstract') #FIXME
319 def append_revision(self, *revision_ids):
320 raise NotImplementedError('append_revision is abstract') #FIXME
322 def working_tree(self):
323 if self.path is None:
324 raise NoWorkingTree(self.base)
326 return SvnWorkingTree(self.path,branch=self)
328 # FIXME: perhaps move these four to a 'ForeignBranch' class in
330 def get_revision_xml(self, revision_id):
331 return bzrlib.xml5.serializer_v5.write_revision_to_string(self.get_revision(revision_id))
333 def get_inventory_xml(self, revision_id):
334 return bzrlib.xml5.serializer_v5.write_inventory_to_string(self.get_inventory(revision_id))
336 def get_revision_sha1(self, revision_id):
337 return bzrlib.osutils.sha_string(self.get_revision_xml(revision_id))
339 def get_inventory_sha1(self, revision_id):
340 return bzrlib.osutils.sha_string(self.get_inventory_xml(revision_id))