Fix compatibility with 0.92.
[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, Pool
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, create_svn_client
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 = create_svn_client(Pool())
173         svn.client.checkout(bzr_to_svn_url(self.base), to_location, rev, 
174                             True, client_ctx)
175
176         return WorkingTree.open(to_location)
177
178     def create_checkout(self, to_location, revision_id=None, lightweight=False):
179         """See Branch.create_checkout()."""
180         if lightweight:
181             return self._create_lightweight_checkout(to_location, revision_id)
182         else:
183             return self._create_heavyweight_checkout(to_location, revision_id)
184
185     def generate_revision_id(self, revnum):
186         """Generate a new revision id for a revision on this branch."""
187         assert isinstance(revnum, int)
188         return self.repository.generate_revision_id(
189                 revnum, self.get_branch_path(revnum), str(self.scheme))
190        
191     def _generate_revision_history(self, last_revnum):
192         """Generate the revision history up until a specified revision."""
193         revhistory = []
194         for (branch, rev) in self.repository.follow_branch(
195                 self.get_branch_path(last_revnum), last_revnum, self.scheme):
196             revhistory.append(
197                 self.repository.generate_revision_id(rev, branch, 
198                     str(self.scheme)))
199         revhistory.reverse()
200         return revhistory
201
202     def _get_nick(self):
203         """Find the nick name for this branch.
204
205         :return: Branch nick
206         """
207         bp = self._branch_path.strip("/")
208         if self._branch_path == "":
209             return None
210         return bp
211
212     nick = property(_get_nick)
213
214     def set_revision_history(self, rev_history):
215         """See Branch.set_revision_history()."""
216         raise NotImplementedError(self.set_revision_history)
217
218     def set_last_revision_info(self, revno, revid):
219         """See Branch.set_last_revision_info()."""
220
221     def last_revision_info(self):
222         """See Branch.last_revision_info()."""
223         last_revid = self.last_revision()
224         return self.revision_id_to_revno(last_revid), last_revid
225
226     def revno(self):
227         """See Branch.revno()."""
228         return self.last_revision_info()[0]
229
230     def revision_id_to_revno(self, revision_id):
231         """See Branch.revision_id_to_revno()."""
232         if revision_id is None:
233             return 0
234         revno = self.repository.revmap.lookup_dist_to_origin(revision_id)
235         if revno is not None:
236             return revno
237         history = self.revision_history()
238         try:
239             return history.index(revision_id) + 1
240         except ValueError:
241             raise NoSuchRevision(self, revision_id)
242
243     def set_push_location(self, location):
244         """See Branch.set_push_location()."""
245         raise NotImplementedError(self.set_push_location)
246
247     def get_push_location(self):
248         """See Branch.get_push_location()."""
249         # get_push_location not supported on Subversion
250         return None
251
252     def revision_history(self, last_revnum=None):
253         """See Branch.revision_history()."""
254         if last_revnum is None:
255             last_revnum = self.get_revnum()
256         if (self._revision_history is None or 
257             self._revision_history_revnum != last_revnum):
258             self._revision_history = self._generate_revision_history(last_revnum)
259             self._revision_history_revnum = last_revnum
260             self.repository.revmap.insert_revision_history(self._revision_history)
261         return self._revision_history
262
263     def last_revision(self):
264         """See Branch.last_revision()."""
265         # Shortcut for finding the tip. This avoids expensive generation time
266         # on large branches.
267         last_revnum = self.get_revnum()
268         if (self._revision_history is None or 
269             self._revision_history_revnum != last_revnum):
270             for (branch, rev) in self.repository.follow_branch(
271                 self.get_branch_path(), last_revnum, self.scheme):
272                 return self.repository.generate_revision_id(rev, branch, 
273                                                             str(self.scheme))
274             return NULL_REVISION
275
276         ph = self.revision_history(last_revnum)
277         if ph:
278             return ph[-1]
279         else:
280             return NULL_REVISION
281
282     def pull(self, source, overwrite=False, stop_revision=None, 
283              _hook_master=None, run_hooks=True):
284         """See Branch.pull()."""
285         result = PullResult()
286         result.source_branch = source
287         result.master_branch = None
288         result.target_branch = self
289         source.lock_read()
290         try:
291             (result.old_revno, result.old_revid) = self.last_revision_info()
292             try:
293                 self.update_revisions(source, stop_revision)
294             except DivergedBranches:
295                 if overwrite:
296                     raise NotImplementedError('overwrite not supported for '
297                                               'Subversion branches')
298                 raise
299             (result.new_revno, result.new_revid) = self.last_revision_info()
300             return result
301         finally:
302             source.unlock()
303
304     def generate_revision_history(self, revision_id, last_rev=None, 
305         other_branch=None):
306         """Create a new revision history that will finish with revision_id.
307         
308         :param revision_id: the new tip to use.
309         :param last_rev: The previous last_revision. If not None, then this
310             must be a ancestory of revision_id, or DivergedBranches is raised.
311         :param other_branch: The other branch that DivergedBranches should
312             raise with respect to.
313         """
314         # stop_revision must be a descendant of last_revision
315         # make a new revision history from the graph
316
317     def _synchronize_history(self, destination, revision_id):
318         """Synchronize last revision and revision history between branches.
319
320         This version is most efficient when the destination is also a
321         BzrBranch6, but works for BzrBranch5, as long as the destination's
322         repository contains all the lefthand ancestors of the intended
323         last_revision.  If not, set_last_revision_info will fail.
324
325         :param destination: The branch to copy the history into
326         :param revision_id: The revision-id to truncate history at.  May
327           be None to copy complete history.
328         """
329         if revision_id is None:
330             revno, revision_id = self.last_revision_info()
331         else:
332             revno = self.revision_id_to_revno(revision_id)
333         destination.set_last_revision_info(revno, revision_id)
334
335     def update_revisions(self, other, stop_revision=None):
336         """See Branch.update_revisions()."""
337         if stop_revision is None:
338             stop_revision = ensure_null(other.last_revision())
339         if (self.last_revision() == stop_revision or
340             self.last_revision() == other.last_revision()):
341             return
342         if not other.repository.get_graph().is_ancestor(self.last_revision(), 
343                                                         stop_revision):
344             if self.repository.get_graph().is_ancestor(stop_revision, 
345                                                        self.last_revision()):
346                 return
347             raise DivergedBranches(self, other)
348         todo = self.repository.lhs_missing_revisions(other.revision_history(), 
349                                                      stop_revision)
350         pb = ui.ui_factory.nested_progress_bar()
351         try:
352             for revid in todo:
353                 pb.update("pushing revisions", todo.index(revid), 
354                           len(todo))
355                 push(self, other, revid)
356         finally:
357             pb.finished()
358
359     def lock_write(self):
360         """See Branch.lock_write()."""
361         # TODO: Obtain lock on the remote server?
362         if self._lock_mode:
363             assert self._lock_mode == 'w'
364             self._lock_count += 1
365         else:
366             self._lock_mode = 'w'
367             self._lock_count = 1
368         
369     def lock_read(self):
370         """See Branch.lock_read()."""
371         if self._lock_mode:
372             assert self._lock_mode in ('r', 'w')
373             self._lock_count += 1
374         else:
375             self._lock_mode = 'r'
376             self._lock_count = 1
377
378     def unlock(self):
379         """See Branch.unlock()."""
380         self._lock_count -= 1
381         if self._lock_count == 0:
382             self._lock_mode = None
383             self._cached_revnum = None
384
385     def get_parent(self):
386         """See Branch.get_parent()."""
387         return self.base
388
389     def set_parent(self, url):
390         """See Branch.set_parent()."""
391
392     def append_revision(self, *revision_ids):
393         """See Branch.append_revision()."""
394         #raise NotImplementedError(self.append_revision)
395         #FIXME: Make sure the appended revision is already 
396         # part of the revision history
397
398     def get_physical_lock_status(self):
399         """See Branch.get_physical_lock_status()."""
400         return False
401
402     def sprout(self, to_bzrdir, revision_id=None):
403         """See Branch.sprout()."""
404         result = to_bzrdir.create_branch()
405         self.copy_content_into(result, revision_id=revision_id)
406         return result
407
408     def __str__(self):
409         return '%s(%r)' % (self.__class__.__name__, self.base)
410
411     __repr__ = __str__
412
413
414 class SvnBranchFormat(BranchFormat):
415     """Branch format for Subversion Branches."""
416     def __init__(self):
417         BranchFormat.__init__(self)
418
419     def __get_matchingbzrdir(self):
420         """See BranchFormat.__get_matchingbzrdir()."""
421         from remote import SvnRemoteFormat
422         return SvnRemoteFormat()
423
424     _matchingbzrdir = property(__get_matchingbzrdir)
425
426     def get_format_description(self):
427         """See BranchFormat.get_format_description."""
428         return 'Subversion Smart Server'
429
430     def get_format_string(self):
431         """See BranchFormat.get_format_string()."""
432         return 'Subversion Smart Server'
433
434     def initialize(self, to_bzrdir):
435         """See BranchFormat.initialize()."""
436         raise NotImplementedError(self.initialize)
437