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 3 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."""
18 from bzrlib import ui, urlutils
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, UnstackableBranchFormat)
23 from bzrlib.revision import is_null, ensure_null
24 from bzrlib.workingtree import WorkingTree
26 from bzrlib.plugins.svn import core, wc
27 from bzrlib.plugins.svn.commit import push, push_ancestors
28 from bzrlib.plugins.svn.config import BranchConfig
29 from bzrlib.plugins.svn.core import SubversionException
30 from bzrlib.plugins.svn.errors import NotSvnBranchPath, ERR_FS_NO_SUCH_REVISION
31 from bzrlib.plugins.svn.foreign import FakeControlFiles
32 from bzrlib.plugins.svn.format import get_rich_root_format
33 from bzrlib.plugins.svn.repository import SvnRepository
34 from bzrlib.plugins.svn.tags import SubversionTags
35 from bzrlib.plugins.svn.transport import bzr_to_svn_url
39 class SvnBranch(Branch):
40 """Maps to a Branch in a Subversion repository """
41 def __init__(self, repository, branch_path, _skip_check=False):
42 """Instantiate a new SvnBranch.
44 :param repos: SvnRepository this branch is part of.
45 :param branch_path: Relative path inside the repository this
47 :param revnum: Subversion revision number of the branch to
48 look at; none for latest.
50 self.repository = repository
51 super(SvnBranch, self).__init__()
52 assert isinstance(self.repository, SvnRepository)
53 self.control_files = FakeControlFiles()
54 self._format = SvnBranchFormat()
55 self._lock_mode = None
57 self.mapping = self.repository.get_mapping()
58 self.layout = self.repository.get_layout()
59 self._branch_path = branch_path.strip("/")
60 self.base = urlutils.join(self.repository.base,
61 self._branch_path).rstrip("/")
62 self._revmeta_cache = None
63 assert isinstance(self._branch_path, str)
66 revnum = self.get_revnum()
67 if self.repository.transport.check_path(self._branch_path,
68 revnum) != core.NODE_DIR:
69 raise NotBranchError(self.base)
70 except SubversionException, (_, num):
71 if num == ERR_FS_NO_SUCH_REVISION:
72 raise NotBranchError(self.base)
74 (type, self.project, _, ip) = self.layout.parse(branch_path)
75 # FIXME: Don't allow tag here
76 if type not in ('branch', 'tag') or ip != '':
77 raise NotSvnBranchPath(branch_path, mapping=self.mapping)
80 return SubversionTags(self)
82 def set_branch_path(self, branch_path):
83 """Change the branch path for this branch.
85 :param branch_path: New branch path.
87 self._branch_path = branch_path.strip("/")
89 def _get_append_revisions_only(self):
90 value = self.get_config().get_user_option('append_revisions_only')
91 return value == 'True'
93 def unprefix(self, relpath):
94 """Remove the branch path from a relpath.
96 :param relpath: path from the repository root.
98 assert relpath.startswith(self.get_branch_path()), \
99 "expected %s prefix, got %s" % (self.get_branch_path(), relpath)
100 return relpath[len(self.get_branch_path()):].strip("/")
102 def get_branch_path(self, revnum=None):
103 """Find the branch path of this branch in the specified revnum.
105 :param revnum: Revnum to look for.
108 return self._branch_path
110 if revnum == self.get_revnum():
111 return self._branch_path
113 # Use revnum - this branch may have been moved in the past
114 return self.repository.transport.get_locations(
115 self._branch_path, self.get_revnum(),
116 [revnum])[revnum].strip("/")
118 def get_revnum(self):
119 """Obtain the Subversion revision number this branch was
122 :return: Revision number
124 if self._lock_mode == 'r' and self._cached_revnum:
125 return self._cached_revnum
126 latest_revnum = self.repository.get_latest_revnum()
127 self._cached_revnum = self.repository._log.find_latest_change(
128 self.get_branch_path(), latest_revnum)
129 if self._cached_revnum is None:
130 raise NotBranchError(self.base)
131 return self._cached_revnum
136 Doesn't do anything for Subversion repositories at the moment (yet).
138 return BranchCheckResult(self)
140 def _create_heavyweight_checkout(self, to_location, revision_id=None,
142 """Create a new heavyweight checkout of this branch.
144 :param to_location: URL of location to create the new checkout in.
145 :param revision_id: Revision that should be the tip of the checkout.
146 :param hardlink: Whether to hardlink
147 :return: WorkingTree object of checkout.
149 checkout_branch = BzrDir.create_branch_convenience(
150 to_location, force_new_tree=False, format=get_rich_root_format())
151 checkout = checkout_branch.bzrdir
152 checkout_branch.bind(self)
153 # pull up to the specified revision_id to set the initial
154 # branch tip correctly, and seed it with history.
155 checkout_branch.pull(self, stop_revision=revision_id)
156 return checkout.create_workingtree(revision_id, hardlink=hardlink)
158 def lookup_revision_id(self, revid):
159 """Look up the matching Subversion revision number on the mainline of
162 :param revid: Revision id to look up.
163 :return: Revision number on the branch.
164 :raises NoSuchRevision: If the revision id was not found.
166 (bp, revnum, mapping) = self.repository.lookup_revision_id(revid,
167 ancestry=(self.get_branch_path(), self.get_revnum()))
168 assert bp.strip("/") == self.get_branch_path(revnum).strip("/"), \
169 "Got %r, expected %r" % (bp, self.get_branch_path(revnum))
172 def _create_lightweight_checkout(self, to_location, revision_id=None):
173 """Create a new lightweight checkout of this branch.
175 :param to_location: URL of location to create the checkout in.
176 :param revision_id: Tip of the checkout.
177 :return: WorkingTree object of the checkout.
179 from bzrlib.plugins.svn.workingtree import update_wc
180 if revision_id is not None:
181 revnum = self.lookup_revision_id(revision_id)
183 revnum = self.get_revnum()
185 svn_url = bzr_to_svn_url(self.base)
186 os.mkdir(to_location)
187 wc.ensure_adm(to_location, self.repository.uuid, svn_url,
188 bzr_to_svn_url(self.repository.base), revnum)
189 adm = wc.WorkingCopy(None, to_location, write_lock=True)
191 conn = self.repository.transport.connections.get(svn_url)
193 update_wc(adm, to_location, conn, revnum)
196 self.repository.transport.add_connection(conn)
199 wt = WorkingTree.open(to_location)
202 def create_checkout(self, to_location, revision_id=None, lightweight=False,
203 accelerator_tree=None, hardlink=False):
204 """See Branch.create_checkout()."""
206 return self._create_lightweight_checkout(to_location, revision_id)
208 return self._create_heavyweight_checkout(to_location, revision_id,
211 def generate_revision_id(self, revnum):
212 """Generate a new revision id for a revision on this branch."""
213 assert isinstance(revnum, int)
215 return self.repository.generate_revision_id(
216 revnum, self.get_branch_path(revnum), self.mapping)
217 except SubversionException, (_, num):
218 if num == ERR_FS_NO_SUCH_REVISION:
219 raise NoSuchRevision(self, revnum)
222 def get_config(self):
223 return BranchConfig(self)
226 """Find the nick name for this branch.
230 bp = self._branch_path.strip("/")
231 if self._branch_path == "":
232 return self.base.split("/")[-1]
235 nick = property(_get_nick)
237 def set_revision_history(self, rev_history):
238 """See Branch.set_revision_history()."""
239 if (rev_history == [] or
240 not self.repository.has_revision(rev_history[-1])):
241 raise NotImplementedError("set_revision_history can't add ghosts")
242 push(self.repository.get_graph(),
243 self, self.repository, rev_history[-1])
244 self._clear_cached_state()
246 def set_last_revision_info(self, revno, revid):
247 """See Branch.set_last_revision_info()."""
249 def mainline_missing_revisions(self, other, stop_revision):
250 """Find the revisions missing on the mainline.
252 :param other: Other branch to retrieve revisions from.
253 :param stop_revision: Revision to stop fetching at.
256 lastrevid = self.last_revision()
257 for revid in other.repository.iter_reverse_revision_history(stop_revision):
258 if lastrevid == revid:
261 missing.append(revid)
264 def otherline_missing_revisions(self, other, stop_revision, overwrite=False):
265 """Find the revisions missing on the mainline.
267 :param other: Other branch to retrieve revisions from.
268 :param stop_revision: Revision to stop fetching at.
269 :param overwrite: Whether or not the existing data should be overwritten
272 for revid in other.repository.iter_reverse_revision_history(stop_revision):
273 if self.repository.has_revision(revid):
276 missing.append(revid)
282 def last_revision_info(self):
283 """See Branch.last_revision_info()."""
284 last_revid = self.last_revision()
285 return self.revision_id_to_revno(last_revid), last_revid
287 def revision_id_to_revno(self, revision_id):
288 """Given a revision id, return its revno"""
289 if is_null(revision_id):
291 revmeta_history = self._revision_meta_history()
292 for revmeta in revmeta_history:
293 if revmeta.get_revision_id(self.mapping) == revision_id:
294 return len(revmeta_history) - revmeta_history.index(revmeta)
295 raise NoSuchRevision(self, revision_id)
297 def get_root_id(self, revnum=None):
299 tree = self.basis_tree()
301 tree = self.repository.revision_tree(self.get_rev_id(revnum))
302 return tree.get_root_id()
304 def set_push_location(self, location):
305 """See Branch.set_push_location()."""
306 raise NotImplementedError(self.set_push_location)
308 def get_push_location(self):
309 """See Branch.get_push_location()."""
310 # get_push_location not supported on Subversion
313 def _revision_meta_history(self):
314 if self._revmeta_cache is None:
315 pb = ui.ui_factory.nested_progress_bar()
317 self._revmeta_cache = list(self.repository.iter_reverse_branch_changes(self.get_branch_path(), self.get_revnum(), to_revnum=0, mapping=self.mapping, pb=pb))
320 return self._revmeta_cache
322 def _gen_revision_history(self):
323 """Generate the revision history from last revision
325 pb = ui.ui_factory.nested_progress_bar()
328 for revmeta in self._revision_meta_history():
329 history.append(revmeta.get_revision_id(self.mapping))
335 def last_revision(self):
336 """See Branch.last_revision()."""
337 # Shortcut for finding the tip. This avoids expensive generation time
339 return self.generate_revision_id(self.get_revnum())
341 def dpush(self, target, stop_revision=None):
342 from bzrlib.plugins.svn.commit import dpush
343 return dpush(target, self, stop_revision)
345 def pull(self, source, overwrite=False, stop_revision=None,
346 _hook_master=None, run_hooks=True, _push_merged=None):
347 """See Branch.pull()."""
348 result = PullResult()
349 result.source_branch = source
350 result.master_branch = None
351 result.target_branch = self
354 (result.old_revno, result.old_revid) = self.last_revision_info()
355 self.update_revisions(source, stop_revision, overwrite,
356 _push_merged=_push_merged)
357 result.tag_conflicts = source.tags.merge_to(self.tags, overwrite)
358 (result.new_revno, result.new_revid) = self.last_revision_info()
363 def generate_revision_history(self, revision_id, last_rev=None,
365 """Create a new revision history that will finish with revision_id.
367 :param revision_id: the new tip to use.
368 :param last_rev: The previous last_revision. If not None, then this
369 must be a ancestory of revision_id, or DivergedBranches is raised.
370 :param other_branch: The other branch that DivergedBranches should
371 raise with respect to.
373 # stop_revision must be a descendant of last_revision
374 # make a new revision history from the graph
376 def _synchronize_history(self, destination, revision_id):
377 """Synchronize last revision and revision history between branches.
379 This version is most efficient when the destination is also a
380 BzrBranch6, but works for BzrBranch5, as long as the destination's
381 repository contains all the lefthand ancestors of the intended
382 last_revision. If not, set_last_revision_info will fail.
384 :param destination: The branch to copy the history into
385 :param revision_id: The revision-id to truncate history at. May
386 be None to copy complete history.
388 if revision_id is None:
389 revno, revision_id = self.last_revision_info()
391 revno = self.revision_id_to_revno(revision_id)
392 destination.set_last_revision_info(revno, revision_id)
394 def update_revisions(self, other, stop_revision=None, overwrite=False,
395 graph=None, _push_merged=False):
396 """See Branch.update_revisions()."""
397 if stop_revision is None:
398 stop_revision = ensure_null(other.last_revision())
399 if (self.last_revision() == stop_revision or
400 self.last_revision() == other.last_revision()):
403 graph = self.repository.get_graph()
404 other_graph = other.repository.get_graph()
405 if not other_graph.is_ancestor(self.last_revision(),
407 if graph.is_ancestor(stop_revision, self.last_revision()):
410 raise DivergedBranches(self, other)
411 todo = self.mainline_missing_revisions(other, stop_revision)
413 # Not possible to add cleanly onto mainline, perhaps need a replace operation
414 todo = self.otherline_missing_revisions(other, stop_revision, overwrite)
416 raise DivergedBranches(self, other)
417 if _push_merged is None:
418 _push_merged = self.layout.push_merged_revisions(self.project)
419 self._push_missing_revisions(graph, other, other_graph, todo,
422 def _push_missing_revisions(self, my_graph, other, other_graph, todo,
424 pb = ui.ui_factory.nested_progress_bar()
427 pb.update("pushing revisions", todo.index(revid),
430 parent_revids = other_graph.get_parent_map([revid])[revid]
431 push_ancestors(self.repository, other.repository, self.layout, self.project, parent_revids, other_graph)
432 push(my_graph, self, other.repository, revid)
433 self._clear_cached_state()
437 def lock_write(self):
438 """See Branch.lock_write()."""
439 # TODO: Obtain lock on the remote server?
441 assert self._lock_mode == 'w'
442 self._lock_count += 1
444 self._lock_mode = 'w'
446 self.repository.lock_write()
449 """See Branch.lock_read()."""
451 assert self._lock_mode in ('r', 'w')
452 self._lock_count += 1
454 self._lock_mode = 'r'
456 self.repository.lock_read()
459 """See Branch.unlock()."""
460 self._lock_count -= 1
461 if self._lock_count == 0:
462 self._lock_mode = None
463 self._clear_cached_state()
464 self.repository.unlock()
466 def _clear_cached_state(self):
467 super(SvnBranch, self)._clear_cached_state()
468 self._cached_revnum = None
469 self._revmeta_cache = None
471 def get_parent(self):
472 """See Branch.get_parent()."""
475 def set_parent(self, url):
476 """See Branch.set_parent()."""
478 def get_physical_lock_status(self):
479 """See Branch.get_physical_lock_status()."""
482 def sprout(self, to_bzrdir, revision_id=None):
483 """See Branch.sprout()."""
484 result = to_bzrdir.create_branch()
485 self.copy_content_into(result, revision_id=revision_id)
486 result.set_parent(self.bzrdir.root_transport.base)
489 def get_stacked_on_url(self):
490 raise UnstackableBranchFormat(self._format, self.base)
493 return '%s(%r)' % (self.__class__.__name__, self.base)
495 def supports_tags(self):
496 return self._format.supports_tags()
501 class SvnBranchFormat(BranchFormat):
502 """Branch format for Subversion Branches."""
504 BranchFormat.__init__(self)
506 def __get_matchingbzrdir(self):
507 """See BranchFormat.__get_matchingbzrdir()."""
508 from remote import SvnRemoteFormat
509 return SvnRemoteFormat()
511 _matchingbzrdir = property(__get_matchingbzrdir)
513 def get_format_description(self):
514 """See BranchFormat.get_format_description."""
515 return 'Subversion Smart Server'
517 def get_format_string(self):
518 """See BranchFormat.get_format_string()."""
519 return 'Subversion Smart Server'
521 def initialize(self, to_bzrdir):
522 """See BranchFormat.initialize()."""
523 raise NotImplementedError(self.initialize)
525 def supports_tags(self):