Avoid tracebacks on malformed data errors.
[jelmer/subvertpy.git] / branch.py
1 # Copyright (C) 2005-2007 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 """Handles branch-specific operations."""
17
18 from bzrlib import ui
19 from bzrlib.branch import Branch, BranchFormat, BranchCheckResult, PullResult
20 from bzrlib.bzrdir import BzrDir
21 from bzrlib.errors import (NoSuchFile, DivergedBranches, NoSuchRevision, 
22                            NotBranchError)
23 from bzrlib.inventory import (Inventory)
24 from bzrlib.revision import ensure_null
25 from bzrlib.workingtree import WorkingTree
26
27 import svn.client, svn.core
28 from svn.core import SubversionException
29
30 from commit import push
31 from errors import NotSvnBranchPath
32 from format import get_rich_root_format
33 from repository import SvnRepository
34 from transport import bzr_to_svn_url, svn_config
35
36
37 class FakeControlFiles(object):
38     """Dummy implementation of ControlFiles.
39     
40     This is required as some code relies on controlfiles being 
41     available."""
42     def get_utf8(self, name):
43         raise NoSuchFile(name)
44
45     def get(self, name):
46         raise NoSuchFile(name)
47
48     def break_lock(self):
49         pass
50
51
52 class SvnBranch(Branch):
53     """Maps to a Branch in a Subversion repository """
54     def __init__(self, base, repository, branch_path):
55         """Instantiate a new SvnBranch.
56
57         :param repos: SvnRepository this branch is part of.
58         :param branch_path: Relative path inside the repository this
59             branch is located at.
60         :param revnum: Subversion revision number of the branch to 
61             look at; none for latest.
62         """
63         super(SvnBranch, self).__init__()
64         self.repository = repository
65         assert isinstance(self.repository, SvnRepository)
66         self.control_files = FakeControlFiles()
67         self.base = base.rstrip("/")
68         self._format = SvnBranchFormat()
69         self._lock_mode = None
70         self._lock_count = 0
71         self._cached_revnum = None
72         self._revision_history = None
73         self._revision_history_revnum = None
74         self.scheme = self.repository.get_scheme()
75         self._branch_path = branch_path.strip("/")
76         try:
77             if self.repository.transport.check_path(branch_path.strip("/"), 
78                 self.get_revnum()) != svn.core.svn_node_dir:
79                 raise NotBranchError(self.base)
80         except SubversionException, (_, num):
81             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
82                 raise NotBranchError(self.base)
83             raise
84         if (not self.scheme.is_branch(branch_path) and 
85             not self.scheme.is_tag(branch_path)):
86             raise NotSvnBranchPath(branch_path, scheme=self.scheme)
87
88     def set_branch_path(self, branch_path):
89         """Change the branch path for this branch.
90
91         :param branch_path: New branch path.
92         """
93         self._branch_path = branch_path.strip("/")
94
95     def get_branch_path(self, revnum=None):
96         """Find the branch path of this branch in the specified revnum.
97
98         :param revnum: Revnum to look for.
99         """
100         if revnum is None:
101             return self._branch_path
102
103         # TODO: Use revnum - this branch may have been moved in the past 
104         return self._branch_path
105
106     def get_revnum(self):
107         """Obtain the Subversion revision number this branch was 
108         last changed in.
109
110         :return: Revision number
111         """
112         if self._lock_mode == 'r' and self._cached_revnum:
113             return self._cached_revnum
114         self._cached_revnum = self.repository.transport.get_latest_revnum()
115         return self._cached_revnum
116
117     def check(self):
118         """See Branch.Check.
119
120         Doesn't do anything for Subversion repositories at the moment (yet).
121         """
122         return BranchCheckResult(self)
123
124     def _create_heavyweight_checkout(self, to_location, revision_id=None):
125         """Create a new heavyweight checkout of this branch.
126
127         :param to_location: URL of location to create the new checkout in.
128         :param revision_id: Revision that should be the tip of the checkout.
129         :return: WorkingTree object of checkout.
130         """
131         checkout_branch = BzrDir.create_branch_convenience(
132             to_location, force_new_tree=False, format=get_rich_root_format())
133         checkout = checkout_branch.bzrdir
134         checkout_branch.bind(self)
135         # pull up to the specified revision_id to set the initial 
136         # branch tip correctly, and seed it with history.
137         checkout_branch.pull(self, stop_revision=revision_id)
138         return checkout.create_workingtree(revision_id)
139
140     def lookup_revision_id(self, revid):
141         """Look up the matching Subversion revision number on the mainline of 
142         the branch.
143
144         :param revid: Revision id to look up.
145         :return: Revision number on the branch. 
146         :raises NoSuchRevision: If the revision id was not found.
147         """
148         (bp, revnum, scheme) = self.repository.lookup_revision_id(revid, 
149                                                              scheme=self.scheme)
150         assert bp.strip("/") == self.get_branch_path(revnum).strip("/"), \
151                 "Got %r, expected %r" % (bp, self.get_branch_path(revnum))
152         return revnum
153
154     def _create_lightweight_checkout(self, to_location, revision_id=None):
155         """Create a new lightweight checkout of this branch.
156
157         :param to_location: URL of location to create the checkout in.
158         :param revision_id: Tip of the checkout.
159         :return: WorkingTree object of the checkout.
160         """
161         peg_rev = svn.core.svn_opt_revision_t()
162         peg_rev.kind = svn.core.svn_opt_revision_head
163
164         rev = svn.core.svn_opt_revision_t()
165         if revision_id is None:
166             rev.kind = svn.core.svn_opt_revision_head
167         else:
168             revnum = self.lookup_revision_id(revision_id)
169             rev.kind = svn.core.svn_opt_revision_number
170             rev.value.number = revnum
171
172         client_ctx = svn.client.create_context()
173         client_ctx.config = svn_config
174         svn.client.checkout(bzr_to_svn_url(self.base), to_location, rev, 
175                             True, client_ctx)
176
177         return WorkingTree.open(to_location)
178
179     def create_checkout(self, to_location, revision_id=None, lightweight=False):
180         """See Branch.create_checkout()."""
181         if lightweight:
182             return self._create_lightweight_checkout(to_location, revision_id)
183         else:
184             return self._create_heavyweight_checkout(to_location, revision_id)
185
186     def generate_revision_id(self, revnum):
187         """Generate a new revision id for a revision on this branch."""
188         assert isinstance(revnum, int)
189         return self.repository.generate_revision_id(
190                 revnum, self.get_branch_path(revnum), str(self.scheme))
191        
192     def _generate_revision_history(self, last_revnum):
193         """Generate the revision history up until a specified revision."""
194         revhistory = []
195         for (branch, rev) in self.repository.follow_branch(
196                 self.get_branch_path(last_revnum), last_revnum, self.scheme):
197             revhistory.append(
198                 self.repository.generate_revision_id(rev, branch, 
199                     str(self.scheme)))
200         revhistory.reverse()
201         return revhistory
202
203     def _get_nick(self):
204         """Find the nick name for this branch.
205
206         :return: Branch nick
207         """
208         bp = self._branch_path.strip("/")
209         if self._branch_path == "":
210             return None
211         return bp
212
213     nick = property(_get_nick)
214
215     def set_revision_history(self, rev_history):
216         """See Branch.set_revision_history()."""
217         raise NotImplementedError(self.set_revision_history)
218
219     def set_last_revision_info(self, revno, revid):
220         """See Branch.set_last_revision_info()."""
221
222     def last_revision_info(self):
223         """See Branch.last_revision_info()."""
224         last_revid = self.last_revision()
225         return self.revision_id_to_revno(last_revid), last_revid
226
227     def revno(self):
228         """See Branch.revno()."""
229         return self.last_revision_info()[0]
230
231     def revision_id_to_revno(self, revision_id):
232         """See Branch.revision_id_to_revno()."""
233         if revision_id is None:
234             return 0
235         revno = self.repository.revmap.lookup_dist_to_origin(revision_id)
236         if revno is not None:
237             return revno
238         history = self.revision_history()
239         try:
240             return history.index(revision_id) + 1
241         except ValueError:
242             raise NoSuchRevision(self, revision_id)
243
244     def set_push_location(self, location):
245         """See Branch.set_push_location()."""
246         raise NotImplementedError(self.set_push_location)
247
248     def get_push_location(self):
249         """See Branch.get_push_location()."""
250         # get_push_location not supported on Subversion
251         return None
252
253     def revision_history(self, last_revnum=None):
254         """See Branch.revision_history()."""
255         if last_revnum is None:
256             last_revnum = self.get_revnum()
257         if (self._revision_history is None or 
258             self._revision_history_revnum != last_revnum):
259             self._revision_history = self._generate_revision_history(last_revnum)
260             self._revision_history_revnum = last_revnum
261             self.repository.revmap.insert_revision_history(self._revision_history)
262         return self._revision_history
263
264     def last_revision(self):
265         """See Branch.last_revision()."""
266         # Shortcut for finding the tip. This avoids expensive generation time
267         # on large branches.
268         last_revnum = self.get_revnum()
269         if (self._revision_history is None or 
270             self._revision_history_revnum != last_revnum):
271             for (branch, rev) in self.repository.follow_branch(
272                 self.get_branch_path(), last_revnum, self.scheme):
273                 return self.repository.generate_revision_id(rev, branch, 
274                                                             str(self.scheme))
275             return NULL_REVISION
276
277         ph = self.revision_history(last_revnum)
278         if ph:
279             return ph[-1]
280         else:
281             return NULL_REVISION
282
283     def pull(self, source, overwrite=False, stop_revision=None, 
284              _hook_master=None, run_hooks=True):
285         """See Branch.pull()."""
286         result = PullResult()
287         result.source_branch = source
288         result.master_branch = None
289         result.target_branch = self
290         source.lock_read()
291         try:
292             (result.old_revno, result.old_revid) = self.last_revision_info()
293             try:
294                 self.update_revisions(source, stop_revision)
295             except DivergedBranches:
296                 if overwrite:
297                     raise NotImplementedError('overwrite not supported for '
298                                               'Subversion branches')
299                 raise
300             (result.new_revno, result.new_revid) = self.last_revision_info()
301             return result
302         finally:
303             source.unlock()
304
305     def generate_revision_history(self, revision_id, last_rev=None, 
306         other_branch=None):
307         """Create a new revision history that will finish with revision_id.
308         
309         :param revision_id: the new tip to use.
310         :param last_rev: The previous last_revision. If not None, then this
311             must be a ancestory of revision_id, or DivergedBranches is raised.
312         :param other_branch: The other branch that DivergedBranches should
313             raise with respect to.
314         """
315         # stop_revision must be a descendant of last_revision
316         # make a new revision history from the graph
317
318     def _synchronize_history(self, destination, revision_id):
319         """Synchronize last revision and revision history between branches.
320
321         This version is most efficient when the destination is also a
322         BzrBranch6, but works for BzrBranch5, as long as the destination's
323         repository contains all the lefthand ancestors of the intended
324         last_revision.  If not, set_last_revision_info will fail.
325
326         :param destination: The branch to copy the history into
327         :param revision_id: The revision-id to truncate history at.  May
328           be None to copy complete history.
329         """
330         if revision_id is None:
331             revno, revision_id = self.last_revision_info()
332         else:
333             revno = self.revision_id_to_revno(revision_id)
334         destination.set_last_revision_info(revno, revision_id)
335
336     def update_revisions(self, other, stop_revision=None):
337         """See Branch.update_revisions()."""
338         if stop_revision is None:
339             stop_revision = ensure_null(other.last_revision())
340         if (self.last_revision() == stop_revision or
341             self.last_revision() == other.last_revision()):
342             return
343         if not other.repository.get_graph().is_ancestor(self.last_revision(), 
344                                                         stop_revision):
345             if self.repository.get_graph().is_ancestor(stop_revision, 
346                                                        self.last_revision()):
347                 return
348             raise DivergedBranches(self, other)
349         todo = self.repository.lhs_missing_revisions(other.revision_history(), 
350                                                      stop_revision)
351         pb = ui.ui_factory.nested_progress_bar()
352         try:
353             for revid in todo:
354                 pb.update("pushing revisions", todo.index(revid), 
355                           len(todo))
356                 push(self, other, revid)
357         finally:
358             pb.finished()
359
360     def lock_write(self):
361         """See Branch.lock_write()."""
362         # TODO: Obtain lock on the remote server?
363         if self._lock_mode:
364             assert self._lock_mode == 'w'
365             self._lock_count += 1
366         else:
367             self._lock_mode = 'w'
368             self._lock_count = 1
369         
370     def lock_read(self):
371         """See Branch.lock_read()."""
372         if self._lock_mode:
373             assert self._lock_mode in ('r', 'w')
374             self._lock_count += 1
375         else:
376             self._lock_mode = 'r'
377             self._lock_count = 1
378
379     def unlock(self):
380         """See Branch.unlock()."""
381         self._lock_count -= 1
382         if self._lock_count == 0:
383             self._lock_mode = None
384             self._cached_revnum = None
385
386     def get_parent(self):
387         """See Branch.get_parent()."""
388         return self.base
389
390     def set_parent(self, url):
391         """See Branch.set_parent()."""
392
393     def append_revision(self, *revision_ids):
394         """See Branch.append_revision()."""
395         #raise NotImplementedError(self.append_revision)
396         #FIXME: Make sure the appended revision is already 
397         # part of the revision history
398
399     def get_physical_lock_status(self):
400         """See Branch.get_physical_lock_status()."""
401         return False
402
403     def sprout(self, to_bzrdir, revision_id=None):
404         """See Branch.sprout()."""
405         result = to_bzrdir.create_branch()
406         self.copy_content_into(result, revision_id=revision_id)
407         return result
408
409     def __str__(self):
410         return '%s(%r)' % (self.__class__.__name__, self.base)
411
412     __repr__ = __str__
413
414
415 class SvnBranchFormat(BranchFormat):
416     """Branch format for Subversion Branches."""
417     def __init__(self):
418         BranchFormat.__init__(self)
419
420     def __get_matchingbzrdir(self):
421         """See BranchFormat.__get_matchingbzrdir()."""
422         from remote import SvnRemoteFormat
423         return SvnRemoteFormat()
424
425     _matchingbzrdir = property(__get_matchingbzrdir)
426
427     def get_format_description(self):
428         """See BranchFormat.get_format_description."""
429         return 'Subversion Smart Server'
430
431     def get_format_string(self):
432         """See BranchFormat.get_format_string()."""
433         return 'Subversion Smart Server'
434
435     def initialize(self, to_bzrdir):
436         """See BranchFormat.initialize()."""
437         raise NotImplementedError(self.initialize)
438