1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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."""
19 from bzrlib.branch import Branch, BranchFormat, BranchCheckResult, PullResult
20 from bzrlib.bzrdir import BzrDir
21 from bzrlib.errors import (NoSuchFile, DivergedBranches, NoSuchRevision,
23 from bzrlib.inventory import (Inventory)
24 from bzrlib.revision import ensure_null
25 from bzrlib.workingtree import WorkingTree
27 import svn.client, svn.core
28 from svn.core import SubversionException
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
37 class FakeControlFiles(object):
38 """Dummy implementation of ControlFiles.
40 This is required as some code relies on controlfiles being
42 def get_utf8(self, name):
43 raise NoSuchFile(name)
46 raise NoSuchFile(name)
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.
57 :param repos: SvnRepository this branch is part of.
58 :param branch_path: Relative path inside the repository this
60 :param revnum: Subversion revision number of the branch to
61 look at; none for latest.
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
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("/")
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)
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)
88 def set_branch_path(self, branch_path):
89 """Change the branch path for this branch.
91 :param branch_path: New branch path.
93 self._branch_path = branch_path.strip("/")
95 def get_branch_path(self, revnum=None):
96 """Find the branch path of this branch in the specified revnum.
98 :param revnum: Revnum to look for.
101 return self._branch_path
103 # TODO: Use revnum - this branch may have been moved in the past
104 return self._branch_path
106 def get_revnum(self):
107 """Obtain the Subversion revision number this branch was
110 :return: Revision number
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
120 Doesn't do anything for Subversion repositories at the moment (yet).
122 return BranchCheckResult(self)
124 def _create_heavyweight_checkout(self, to_location, revision_id=None):
125 """Create a new heavyweight checkout of this branch.
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.
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)
140 def lookup_revision_id(self, revid):
141 """Look up the matching Subversion revision number on the mainline of
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.
148 (bp, revnum, scheme) = self.repository.lookup_revision_id(revid,
150 assert bp.strip("/") == self.get_branch_path(revnum).strip("/"), \
151 "Got %r, expected %r" % (bp, self.get_branch_path(revnum))
154 def _create_lightweight_checkout(self, to_location, revision_id=None):
155 """Create a new lightweight checkout of this branch.
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.
161 peg_rev = svn.core.svn_opt_revision_t()
162 peg_rev.kind = svn.core.svn_opt_revision_head
164 rev = svn.core.svn_opt_revision_t()
165 if revision_id is None:
166 rev.kind = svn.core.svn_opt_revision_head
168 revnum = self.lookup_revision_id(revision_id)
169 rev.kind = svn.core.svn_opt_revision_number
170 rev.value.number = revnum
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,
177 return WorkingTree.open(to_location)
179 def create_checkout(self, to_location, revision_id=None, lightweight=False):
180 """See Branch.create_checkout()."""
182 return self._create_lightweight_checkout(to_location, revision_id)
184 return self._create_heavyweight_checkout(to_location, revision_id)
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))
192 def _generate_revision_history(self, last_revnum):
193 """Generate the revision history up until a specified revision."""
195 for (branch, rev) in self.repository.follow_branch(
196 self.get_branch_path(last_revnum), last_revnum, self.scheme):
198 self.repository.generate_revision_id(rev, branch,
204 """Find the nick name for this branch.
208 bp = self._branch_path.strip("/")
209 if self._branch_path == "":
213 nick = property(_get_nick)
215 def set_revision_history(self, rev_history):
216 """See Branch.set_revision_history()."""
217 raise NotImplementedError(self.set_revision_history)
219 def set_last_revision_info(self, revno, revid):
220 """See Branch.set_last_revision_info()."""
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
228 """See Branch.revno()."""
229 return self.last_revision_info()[0]
231 def revision_id_to_revno(self, revision_id):
232 """See Branch.revision_id_to_revno()."""
233 if revision_id is None:
235 revno = self.repository.revmap.lookup_dist_to_origin(revision_id)
236 if revno is not None:
238 history = self.revision_history()
240 return history.index(revision_id) + 1
242 raise NoSuchRevision(self, revision_id)
244 def set_push_location(self, location):
245 """See Branch.set_push_location()."""
246 raise NotImplementedError(self.set_push_location)
248 def get_push_location(self):
249 """See Branch.get_push_location()."""
250 # get_push_location not supported on Subversion
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
264 def last_revision(self):
265 """See Branch.last_revision()."""
266 # Shortcut for finding the tip. This avoids expensive generation time
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,
277 ph = self.revision_history(last_revnum)
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
292 (result.old_revno, result.old_revid) = self.last_revision_info()
294 self.update_revisions(source, stop_revision)
295 except DivergedBranches:
297 raise NotImplementedError('overwrite not supported for '
298 'Subversion branches')
300 (result.new_revno, result.new_revid) = self.last_revision_info()
305 def generate_revision_history(self, revision_id, last_rev=None,
307 """Create a new revision history that will finish with revision_id.
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.
315 # stop_revision must be a descendant of last_revision
316 # make a new revision history from the graph
318 def _synchronize_history(self, destination, revision_id):
319 """Synchronize last revision and revision history between branches.
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.
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.
330 if revision_id is None:
331 revno, revision_id = self.last_revision_info()
333 revno = self.revision_id_to_revno(revision_id)
334 destination.set_last_revision_info(revno, revision_id)
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()):
343 if not other.repository.get_graph().is_ancestor(self.last_revision(),
345 if self.repository.get_graph().is_ancestor(stop_revision,
346 self.last_revision()):
348 raise DivergedBranches(self, other)
349 todo = self.repository.lhs_missing_revisions(other.revision_history(),
351 pb = ui.ui_factory.nested_progress_bar()
354 pb.update("pushing revisions", todo.index(revid),
356 push(self, other, revid)
360 def lock_write(self):
361 """See Branch.lock_write()."""
362 # TODO: Obtain lock on the remote server?
364 assert self._lock_mode == 'w'
365 self._lock_count += 1
367 self._lock_mode = 'w'
371 """See Branch.lock_read()."""
373 assert self._lock_mode in ('r', 'w')
374 self._lock_count += 1
376 self._lock_mode = 'r'
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
386 def get_parent(self):
387 """See Branch.get_parent()."""
390 def set_parent(self, url):
391 """See Branch.set_parent()."""
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
399 def get_physical_lock_status(self):
400 """See Branch.get_physical_lock_status()."""
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)
410 return '%s(%r)' % (self.__class__.__name__, self.base)
415 class SvnBranchFormat(BranchFormat):
416 """Branch format for Subversion Branches."""
418 BranchFormat.__init__(self)
420 def __get_matchingbzrdir(self):
421 """See BranchFormat.__get_matchingbzrdir()."""
422 from remote import SvnRemoteFormat
423 return SvnRemoteFormat()
425 _matchingbzrdir = property(__get_matchingbzrdir)
427 def get_format_description(self):
428 """See BranchFormat.get_format_description."""
429 return 'Subversion Smart Server'
431 def get_format_string(self):
432 """See BranchFormat.get_format_string()."""
433 return 'Subversion Smart Server'
435 def initialize(self, to_bzrdir):
436 """See BranchFormat.initialize()."""
437 raise NotImplementedError(self.initialize)