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, Pool
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
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 unprefix(self, relpath):
96 """Remove the branch path from a relpath.
98 :param relpath: path from the repository root.
100 assert relpath.startswith(self.get_branch_path())
101 return relpath[len(self.get_branch_path()):].strip("/")
103 def get_branch_path(self, revnum=None):
104 """Find the branch path of this branch in the specified revnum.
106 :param revnum: Revnum to look for.
109 return self._branch_path
111 # TODO: Use revnum - this branch may have been moved in the past
112 return self._branch_path
114 def get_revnum(self):
115 """Obtain the Subversion revision number this branch was
118 :return: Revision number
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
128 Doesn't do anything for Subversion repositories at the moment (yet).
130 return BranchCheckResult(self)
132 def _create_heavyweight_checkout(self, to_location, revision_id=None):
133 """Create a new heavyweight checkout of this branch.
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.
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)
148 def lookup_revision_id(self, revid):
149 """Look up the matching Subversion revision number on the mainline of
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.
156 (bp, revnum, scheme) = self.repository.lookup_revision_id(revid,
158 assert bp.strip("/") == self.get_branch_path(revnum).strip("/"), \
159 "Got %r, expected %r" % (bp, self.get_branch_path(revnum))
162 def _create_lightweight_checkout(self, to_location, revision_id=None):
163 """Create a new lightweight checkout of this branch.
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.
169 peg_rev = svn.core.svn_opt_revision_t()
170 peg_rev.kind = svn.core.svn_opt_revision_head
172 rev = svn.core.svn_opt_revision_t()
173 if revision_id is None:
174 rev.kind = svn.core.svn_opt_revision_head
176 revnum = self.lookup_revision_id(revision_id)
177 rev.kind = svn.core.svn_opt_revision_number
178 rev.value.number = revnum
180 client_ctx = create_svn_client(Pool())
181 svn.client.checkout(bzr_to_svn_url(self.base), to_location, rev,
184 return WorkingTree.open(to_location)
186 def create_checkout(self, to_location, revision_id=None, lightweight=False):
187 """See Branch.create_checkout()."""
189 return self._create_lightweight_checkout(to_location, revision_id)
191 return self._create_heavyweight_checkout(to_location, revision_id)
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))
199 def _generate_revision_history(self, last_revnum):
200 """Generate the revision history up until a specified revision."""
202 for (branch, rev) in self.repository.follow_branch(
203 self.get_branch_path(last_revnum), last_revnum, self.scheme):
205 self.repository.generate_revision_id(rev, branch,
211 """Find the nick name for this branch.
215 bp = self._branch_path.strip("/")
216 if self._branch_path == "":
220 nick = property(_get_nick)
222 def set_revision_history(self, rev_history):
223 """See Branch.set_revision_history()."""
224 raise NotImplementedError(self.set_revision_history)
226 def set_last_revision_info(self, revno, revid):
227 """See Branch.set_last_revision_info()."""
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
235 """See Branch.revno()."""
236 return self.last_revision_info()[0]
238 def revision_id_to_revno(self, revision_id):
239 """See Branch.revision_id_to_revno()."""
240 if revision_id is None:
242 revno = self.repository.revmap.lookup_dist_to_origin(revision_id)
243 if revno is not None:
245 history = self.revision_history()
247 return history.index(revision_id) + 1
249 raise NoSuchRevision(self, revision_id)
251 def set_push_location(self, location):
252 """See Branch.set_push_location()."""
253 raise NotImplementedError(self.set_push_location)
255 def get_push_location(self):
256 """See Branch.get_push_location()."""
257 # get_push_location not supported on Subversion
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
271 def last_revision(self):
272 """See Branch.last_revision()."""
273 # Shortcut for finding the tip. This avoids expensive generation time
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,
284 ph = self.revision_history(last_revnum)
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
299 (result.old_revno, result.old_revid) = self.last_revision_info()
301 self.update_revisions(source, stop_revision)
302 except DivergedBranches:
304 raise NotImplementedError('overwrite not supported for '
305 'Subversion branches')
307 (result.new_revno, result.new_revid) = self.last_revision_info()
312 def generate_revision_history(self, revision_id, last_rev=None,
314 """Create a new revision history that will finish with revision_id.
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.
322 # stop_revision must be a descendant of last_revision
323 # make a new revision history from the graph
325 def _synchronize_history(self, destination, revision_id):
326 """Synchronize last revision and revision history between branches.
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.
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.
337 if revision_id is None:
338 revno, revision_id = self.last_revision_info()
340 revno = self.revision_id_to_revno(revision_id)
341 destination.set_last_revision_info(revno, revision_id)
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()):
350 if not other.repository.get_graph().is_ancestor(self.last_revision(),
352 if self.repository.get_graph().is_ancestor(stop_revision,
353 self.last_revision()):
355 raise DivergedBranches(self, other)
356 todo = self.repository.lhs_missing_revisions(other.revision_history(),
358 pb = ui.ui_factory.nested_progress_bar()
361 pb.update("pushing revisions", todo.index(revid),
363 push(self, other, revid)
367 def lock_write(self):
368 """See Branch.lock_write()."""
369 # TODO: Obtain lock on the remote server?
371 assert self._lock_mode == 'w'
372 self._lock_count += 1
374 self._lock_mode = 'w'
378 """See Branch.lock_read()."""
380 assert self._lock_mode in ('r', 'w')
381 self._lock_count += 1
383 self._lock_mode = 'r'
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
393 def get_parent(self):
394 """See Branch.get_parent()."""
397 def set_parent(self, url):
398 """See Branch.set_parent()."""
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
406 def get_physical_lock_status(self):
407 """See Branch.get_physical_lock_status()."""
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)
417 return '%s(%r)' % (self.__class__.__name__, self.base)
422 class SvnBranchFormat(BranchFormat):
423 """Branch format for Subversion Branches."""
425 BranchFormat.__init__(self)
427 def __get_matchingbzrdir(self):
428 """See BranchFormat.__get_matchingbzrdir()."""
429 from remote import SvnRemoteFormat
430 return SvnRemoteFormat()
432 _matchingbzrdir = property(__get_matchingbzrdir)
434 def get_format_description(self):
435 """See BranchFormat.get_format_description."""
436 return 'Subversion Smart Server'
438 def get_format_string(self):
439 """See BranchFormat.get_format_string()."""
440 return 'Subversion Smart Server'
442 def initialize(self, to_bzrdir):
443 """See BranchFormat.initialize()."""
444 raise NotImplementedError(self.initialize)