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