Remove references to svn.
[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 3 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, 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
25
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
36
37 import os
38
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.
43
44         :param repos: SvnRepository this branch is part of.
45         :param branch_path: Relative path inside the repository this
46             branch is located at.
47         :param revnum: Subversion revision number of the branch to 
48             look at; none for latest.
49         """
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
56         self._lock_count = 0
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)
64         if not _skip_check:
65             try:
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)
73                 raise
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)
78
79     def _make_tags(self):
80         return SubversionTags(self)
81
82     def set_branch_path(self, branch_path):
83         """Change the branch path for this branch.
84
85         :param branch_path: New branch path.
86         """
87         self._branch_path = branch_path.strip("/")
88
89     def _get_append_revisions_only(self):
90         value = self.get_config().get_user_option('append_revisions_only')
91         return value == 'True'
92
93     def unprefix(self, relpath):
94         """Remove the branch path from a relpath.
95
96         :param relpath: path from the repository root.
97         """
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("/")
101
102     def get_branch_path(self, revnum=None):
103         """Find the branch path of this branch in the specified revnum.
104
105         :param revnum: Revnum to look for.
106         """
107         if revnum is None:
108             return self._branch_path
109
110         if revnum == self.get_revnum():
111             return self._branch_path
112
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("/")
117
118     def get_revnum(self):
119         """Obtain the Subversion revision number this branch was 
120         last changed in.
121
122         :return: Revision number
123         """
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
132
133     def check(self):
134         """See Branch.Check.
135
136         Doesn't do anything for Subversion repositories at the moment (yet).
137         """
138         return BranchCheckResult(self)
139
140     def _create_heavyweight_checkout(self, to_location, revision_id=None, 
141                                      hardlink=False):
142         """Create a new heavyweight checkout of this branch.
143
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.
148         """
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)
157
158     def lookup_revision_id(self, revid):
159         """Look up the matching Subversion revision number on the mainline of 
160         the branch.
161
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.
165         """
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))
170         return revnum
171
172     def _create_lightweight_checkout(self, to_location, revision_id=None):
173         """Create a new lightweight checkout of this branch.
174
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.
178         """
179         from bzrlib.plugins.svn.workingtree import update_wc
180         if revision_id is not None:
181             revnum = self.lookup_revision_id(revision_id)
182         else:
183             revnum = self.get_revnum()
184
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)
190         try:
191             conn = self.repository.transport.connections.get(svn_url)
192             try:
193                 update_wc(adm, to_location, conn, revnum)
194             finally:
195                 if not conn.busy:
196                     self.repository.transport.add_connection(conn)
197         finally:
198             adm.close()
199         wt = WorkingTree.open(to_location)
200         return wt
201
202     def create_checkout(self, to_location, revision_id=None, lightweight=False,
203                         accelerator_tree=None, hardlink=False):
204         """See Branch.create_checkout()."""
205         if lightweight:
206             return self._create_lightweight_checkout(to_location, revision_id)
207         else:
208             return self._create_heavyweight_checkout(to_location, revision_id, 
209                                                      hardlink=hardlink)
210
211     def generate_revision_id(self, revnum):
212         """Generate a new revision id for a revision on this branch."""
213         assert isinstance(revnum, int)
214         try:
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)
220             raise
221
222     def get_config(self):
223         return BranchConfig(self)
224        
225     def _get_nick(self):
226         """Find the nick name for this branch.
227
228         :return: Branch nick
229         """
230         bp = self._branch_path.strip("/")
231         if self._branch_path == "":
232             return self.base.split("/")[-1]
233         return bp
234
235     nick = property(_get_nick)
236
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()
245
246     def set_last_revision_info(self, revno, revid):
247         """See Branch.set_last_revision_info()."""
248
249     def mainline_missing_revisions(self, other, stop_revision):
250         """Find the revisions missing on the mainline.
251         
252         :param other: Other branch to retrieve revisions from.
253         :param stop_revision: Revision to stop fetching at.
254         """
255         missing = []
256         lastrevid = self.last_revision()
257         for revid in other.repository.iter_reverse_revision_history(stop_revision):
258             if lastrevid == revid:
259                 missing.reverse()
260                 return missing
261             missing.append(revid)
262         return None
263
264     def otherline_missing_revisions(self, other, stop_revision, overwrite=False):
265         """Find the revisions missing on the mainline.
266         
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
270         """
271         missing = []
272         for revid in other.repository.iter_reverse_revision_history(stop_revision):
273             if self.repository.has_revision(revid):
274                 missing.reverse()
275                 return missing
276             missing.append(revid)
277         if not overwrite:
278             return None
279         else:
280             return missing
281  
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
286
287     def revision_id_to_revno(self, revision_id):
288         """Given a revision id, return its revno"""
289         if is_null(revision_id):
290             return 0
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)
296
297     def get_root_id(self, revnum=None):
298         if revnum is None:
299             tree = self.basis_tree()
300         else:
301             tree = self.repository.revision_tree(self.get_rev_id(revnum))
302         return tree.get_root_id()
303
304     def set_push_location(self, location):
305         """See Branch.set_push_location()."""
306         raise NotImplementedError(self.set_push_location)
307
308     def get_push_location(self):
309         """See Branch.get_push_location()."""
310         # get_push_location not supported on Subversion
311         return None
312
313     def _revision_meta_history(self):
314         if self._revmeta_cache is None:
315             pb = ui.ui_factory.nested_progress_bar()
316             try:
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))
318             finally:
319                 pb.finished()
320         return self._revmeta_cache
321
322     def _gen_revision_history(self):
323         """Generate the revision history from last revision
324         """
325         pb = ui.ui_factory.nested_progress_bar()
326         try:
327             history = []
328             for revmeta in self._revision_meta_history():
329                 history.append(revmeta.get_revision_id(self.mapping))
330         finally:
331             pb.finished()
332         history.reverse()
333         return history
334
335     def last_revision(self):
336         """See Branch.last_revision()."""
337         # Shortcut for finding the tip. This avoids expensive generation time
338         # on large branches.
339         return self.generate_revision_id(self.get_revnum())
340
341     def dpush(self, target, stop_revision=None):
342         from bzrlib.plugins.svn.commit import dpush
343         return dpush(target, self, stop_revision)
344
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
352         source.lock_read()
353         try:
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()
359             return result
360         finally:
361             source.unlock()
362
363     def generate_revision_history(self, revision_id, last_rev=None, 
364         other_branch=None):
365         """Create a new revision history that will finish with revision_id.
366         
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.
372         """
373         # stop_revision must be a descendant of last_revision
374         # make a new revision history from the graph
375
376     def _synchronize_history(self, destination, revision_id):
377         """Synchronize last revision and revision history between branches.
378
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.
383
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.
387         """
388         if revision_id is None:
389             revno, revision_id = self.last_revision_info()
390         else:
391             revno = self.revision_id_to_revno(revision_id)
392         destination.set_last_revision_info(revno, revision_id)
393
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()):
401             return
402         if graph is None:
403             graph = self.repository.get_graph()
404         other_graph = other.repository.get_graph()
405         if not other_graph.is_ancestor(self.last_revision(), 
406                                                         stop_revision):
407             if graph.is_ancestor(stop_revision, self.last_revision()):
408                 return
409             if not overwrite:
410                 raise DivergedBranches(self, other)
411         todo = self.mainline_missing_revisions(other, stop_revision)
412         if todo is None:
413             # Not possible to add cleanly onto mainline, perhaps need a replace operation
414             todo = self.otherline_missing_revisions(other, stop_revision, overwrite)
415         if todo is None:
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, 
420                                      _push_merged)
421
422     def _push_missing_revisions(self, my_graph, other, other_graph, todo, 
423                                 push_merged=False):
424         pb = ui.ui_factory.nested_progress_bar()
425         try:
426             for revid in todo:
427                 pb.update("pushing revisions", todo.index(revid), 
428                           len(todo))
429                 if push_merged:
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()
434         finally:
435             pb.finished()
436
437     def lock_write(self):
438         """See Branch.lock_write()."""
439         # TODO: Obtain lock on the remote server?
440         if self._lock_mode:
441             assert self._lock_mode == 'w'
442             self._lock_count += 1
443         else:
444             self._lock_mode = 'w'
445             self._lock_count = 1
446         self.repository.lock_write()
447         
448     def lock_read(self):
449         """See Branch.lock_read()."""
450         if self._lock_mode:
451             assert self._lock_mode in ('r', 'w')
452             self._lock_count += 1
453         else:
454             self._lock_mode = 'r'
455             self._lock_count = 1
456         self.repository.lock_read()
457
458     def unlock(self):
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()
465
466     def _clear_cached_state(self):
467         super(SvnBranch, self)._clear_cached_state()
468         self._cached_revnum = None
469         self._revmeta_cache = None
470
471     def get_parent(self):
472         """See Branch.get_parent()."""
473         return None
474
475     def set_parent(self, url):
476         """See Branch.set_parent()."""
477
478     def get_physical_lock_status(self):
479         """See Branch.get_physical_lock_status()."""
480         return False
481
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)
487         return result
488
489     def get_stacked_on_url(self):
490         raise UnstackableBranchFormat(self._format, self.base)
491
492     def __str__(self):
493         return '%s(%r)' % (self.__class__.__name__, self.base)
494
495     def supports_tags(self):
496         return self._format.supports_tags()
497
498     __repr__ = __str__
499
500
501 class SvnBranchFormat(BranchFormat):
502     """Branch format for Subversion Branches."""
503     def __init__(self):
504         BranchFormat.__init__(self)
505
506     def __get_matchingbzrdir(self):
507         """See BranchFormat.__get_matchingbzrdir()."""
508         from remote import SvnRemoteFormat
509         return SvnRemoteFormat()
510
511     _matchingbzrdir = property(__get_matchingbzrdir)
512
513     def get_format_description(self):
514         """See BranchFormat.get_format_description."""
515         return 'Subversion Smart Server'
516
517     def get_format_string(self):
518         """See BranchFormat.get_format_string()."""
519         return 'Subversion Smart Server'
520
521     def initialize(self, to_bzrdir):
522         """See BranchFormat.initialize()."""
523         raise NotImplementedError(self.initialize)
524
525     def supports_tags(self):
526         return True