1# Copyright (C) 2005-2012 Canonical Ltd
2# Copyright (C) 2017 Breezy Developers
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18from io import BytesIO
19import sys
20
21from ..lazy_import import lazy_import
22lazy_import(globals(), """
23from breezy import (
24    cache_utf8,
25    config as _mod_config,
26    lockable_files,
27    lockdir,
28    rio,
29    shelf,
30    )
31from breezy.bzr import (
32    tag as _mod_tag,
33    )
34""")
35
36from . import bzrdir
37from .. import (
38    controldir,
39    errors,
40    revision as _mod_revision,
41    urlutils,
42    )
43from ..branch import (
44    Branch,
45    BranchFormat,
46    BranchWriteLockResult,
47    format_registry,
48    UnstackableBranchFormat,
49    )
50from ..decorators import (
51    only_raises,
52    )
53from ..lock import _RelockDebugMixin, LogicalLockResult
54from ..trace import (
55    mutter,
56    )
57
58
59class BzrBranch(Branch, _RelockDebugMixin):
60    """A branch stored in the actual filesystem.
61
62    Note that it's "local" in the context of the filesystem; it doesn't
63    really matter if it's on an nfs/smb/afs/coda/... share, as long as
64    it's writable, and can be accessed via the normal filesystem API.
65
66    :ivar _transport: Transport for file operations on this branch's
67        control files, typically pointing to the .bzr/branch directory.
68    :ivar repository: Repository for this branch.
69    :ivar base: The url of the base directory for this branch; the one
70        containing the .bzr directory.
71    :ivar name: Optional colocated branch name as it exists in the control
72        directory.
73    """
74
75    def __init__(self, _format=None,
76                 _control_files=None, a_controldir=None, name=None,
77                 _repository=None, ignore_fallbacks=False,
78                 possible_transports=None):
79        """Create new branch object at a particular location."""
80        if a_controldir is None:
81            raise ValueError('a_controldir must be supplied')
82        if name is None:
83            raise ValueError('name must be supplied')
84        self.controldir = a_controldir
85        self._user_transport = self.controldir.transport.clone('..')
86        if name != u"":
87            self._user_transport.set_segment_parameter(
88                "branch", urlutils.escape(name))
89        self._base = self._user_transport.base
90        self.name = name
91        self._format = _format
92        if _control_files is None:
93            raise ValueError('BzrBranch _control_files is None')
94        self.control_files = _control_files
95        self._transport = _control_files._transport
96        self.repository = _repository
97        self.conf_store = None
98        Branch.__init__(self, possible_transports)
99        self._tags_bytes = None
100
101    def __str__(self):
102        return '%s(%s)' % (self.__class__.__name__, self.user_url)
103
104    __repr__ = __str__
105
106    def _get_base(self):
107        """Returns the directory containing the control directory."""
108        return self._base
109
110    base = property(_get_base, doc="The URL for the root of this branch.")
111
112    @property
113    def user_transport(self):
114        return self._user_transport
115
116    def _get_config(self):
117        """Get the concrete config for just the config in this branch.
118
119        This is not intended for client use; see Branch.get_config for the
120        public API.
121
122        Added in 1.14.
123
124        :return: An object supporting get_option and set_option.
125        """
126        return _mod_config.TransportConfig(self._transport, 'branch.conf')
127
128    def _get_config_store(self):
129        if self.conf_store is None:
130            self.conf_store = _mod_config.BranchStore(self)
131        return self.conf_store
132
133    def _uncommitted_branch(self):
134        """Return the branch that may contain uncommitted changes."""
135        master = self.get_master_branch()
136        if master is not None:
137            return master
138        else:
139            return self
140
141    def store_uncommitted(self, creator):
142        """Store uncommitted changes from a ShelfCreator.
143
144        :param creator: The ShelfCreator containing uncommitted changes, or
145            None to delete any stored changes.
146        :raises: ChangesAlreadyStored if the branch already has changes.
147        """
148        branch = self._uncommitted_branch()
149        if creator is None:
150            branch._transport.delete('stored-transform')
151            return
152        if branch._transport.has('stored-transform'):
153            raise errors.ChangesAlreadyStored
154        transform = BytesIO()
155        creator.write_shelf(transform)
156        transform.seek(0)
157        branch._transport.put_file('stored-transform', transform)
158
159    def get_unshelver(self, tree):
160        """Return a shelf.Unshelver for this branch and tree.
161
162        :param tree: The tree to use to construct the Unshelver.
163        :return: an Unshelver or None if no changes are stored.
164        """
165        branch = self._uncommitted_branch()
166        try:
167            transform = branch._transport.get('stored-transform')
168        except errors.NoSuchFile:
169            return None
170        return shelf.Unshelver.from_tree_and_shelf(tree, transform)
171
172    def is_locked(self):
173        return self.control_files.is_locked()
174
175    def lock_write(self, token=None):
176        """Lock the branch for write operations.
177
178        :param token: A token to permit reacquiring a previously held and
179            preserved lock.
180        :return: A BranchWriteLockResult.
181        """
182        if not self.is_locked():
183            self._note_lock('w')
184            self.repository._warn_if_deprecated(self)
185            self.repository.lock_write()
186            took_lock = True
187        else:
188            took_lock = False
189        try:
190            return BranchWriteLockResult(
191                self.unlock,
192                self.control_files.lock_write(token=token))
193        except BaseException:
194            if took_lock:
195                self.repository.unlock()
196            raise
197
198    def lock_read(self):
199        """Lock the branch for read operations.
200
201        :return: A breezy.lock.LogicalLockResult.
202        """
203        if not self.is_locked():
204            self._note_lock('r')
205            self.repository._warn_if_deprecated(self)
206            self.repository.lock_read()
207            took_lock = True
208        else:
209            took_lock = False
210        try:
211            self.control_files.lock_read()
212            return LogicalLockResult(self.unlock)
213        except BaseException:
214            if took_lock:
215                self.repository.unlock()
216            raise
217
218    @only_raises(errors.LockNotHeld, errors.LockBroken)
219    def unlock(self):
220        if self.control_files._lock_count == 1 and self.conf_store is not None:
221            self.conf_store.save_changes()
222        try:
223            self.control_files.unlock()
224        finally:
225            if not self.control_files.is_locked():
226                self.repository.unlock()
227                # we just released the lock
228                self._clear_cached_state()
229
230    def peek_lock_mode(self):
231        if self.control_files._lock_count == 0:
232            return None
233        else:
234            return self.control_files._lock_mode
235
236    def get_physical_lock_status(self):
237        return self.control_files.get_physical_lock_status()
238
239    def set_last_revision_info(self, revno, revision_id):
240        if not revision_id or not isinstance(revision_id, bytes):
241            raise errors.InvalidRevisionId(
242                revision_id=revision_id, branch=self)
243        revision_id = _mod_revision.ensure_null(revision_id)
244        with self.lock_write():
245            old_revno, old_revid = self.last_revision_info()
246            if self.get_append_revisions_only():
247                self._check_history_violation(revision_id)
248            self._run_pre_change_branch_tip_hooks(revno, revision_id)
249            self._write_last_revision_info(revno, revision_id)
250            self._clear_cached_state()
251            self._last_revision_info_cache = revno, revision_id
252            self._run_post_change_branch_tip_hooks(old_revno, old_revid)
253
254    def basis_tree(self):
255        """See Branch.basis_tree."""
256        return self.repository.revision_tree(self.last_revision())
257
258    def _get_parent_location(self):
259        _locs = ['parent', 'pull', 'x-pull']
260        for l in _locs:
261            try:
262                contents = self._transport.get_bytes(l)
263            except errors.NoSuchFile:
264                pass
265            else:
266                return contents.strip(b'\n').decode('utf-8')
267        return None
268
269    def get_stacked_on_url(self):
270        raise UnstackableBranchFormat(self._format, self.user_url)
271
272    def set_push_location(self, location):
273        """See Branch.set_push_location."""
274        self.get_config().set_user_option(
275            'push_location', location,
276            store=_mod_config.STORE_LOCATION_NORECURSE)
277
278    def _set_parent_location(self, url):
279        if url is None:
280            self._transport.delete('parent')
281        else:
282            if isinstance(url, str):
283                url = url.encode('utf-8')
284            self._transport.put_bytes('parent', url + b'\n',
285                                      mode=self.controldir._get_file_mode())
286
287    def unbind(self):
288        """If bound, unbind"""
289        with self.lock_write():
290            return self.set_bound_location(None)
291
292    def bind(self, other):
293        """Bind this branch to the branch other.
294
295        This does not push or pull data between the branches, though it does
296        check for divergence to raise an error when the branches are not
297        either the same, or one a prefix of the other. That behaviour may not
298        be useful, so that check may be removed in future.
299
300        :param other: The branch to bind to
301        :type other: Branch
302        """
303        # TODO: jam 20051230 Consider checking if the target is bound
304        #       It is debatable whether you should be able to bind to
305        #       a branch which is itself bound.
306        #       Committing is obviously forbidden,
307        #       but binding itself may not be.
308        #       Since we *have* to check at commit time, we don't
309        #       *need* to check here
310
311        # we want to raise diverged if:
312        # last_rev is not in the other_last_rev history, AND
313        # other_last_rev is not in our history, and do it without pulling
314        # history around
315        with self.lock_write():
316            self.set_bound_location(other.base)
317
318    def get_bound_location(self):
319        try:
320            return self._transport.get_bytes('bound')[:-1].decode('utf-8')
321        except errors.NoSuchFile:
322            return None
323
324    def get_master_branch(self, possible_transports=None):
325        """Return the branch we are bound to.
326
327        :return: Either a Branch, or None
328        """
329        with self.lock_read():
330            if self._master_branch_cache is None:
331                self._master_branch_cache = self._get_master_branch(
332                    possible_transports)
333            return self._master_branch_cache
334
335    def _get_master_branch(self, possible_transports):
336        bound_loc = self.get_bound_location()
337        if not bound_loc:
338            return None
339        try:
340            return Branch.open(bound_loc,
341                               possible_transports=possible_transports)
342        except (errors.NotBranchError, errors.ConnectionError) as e:
343            raise errors.BoundBranchConnectionFailure(
344                self, bound_loc, e)
345
346    def set_bound_location(self, location):
347        """Set the target where this branch is bound to.
348
349        :param location: URL to the target branch
350        """
351        with self.lock_write():
352            self._master_branch_cache = None
353            if location:
354                self._transport.put_bytes(
355                    'bound', location.encode('utf-8') + b'\n',
356                    mode=self.controldir._get_file_mode())
357            else:
358                try:
359                    self._transport.delete('bound')
360                except errors.NoSuchFile:
361                    return False
362                return True
363
364    def update(self, possible_transports=None):
365        """Synchronise this branch with the master branch if any.
366
367        :return: None or the last_revision that was pivoted out during the
368                 update.
369        """
370        with self.lock_write():
371            master = self.get_master_branch(possible_transports)
372            if master is not None:
373                old_tip = _mod_revision.ensure_null(self.last_revision())
374                self.pull(master, overwrite=True)
375                if self.repository.get_graph().is_ancestor(
376                        old_tip, _mod_revision.ensure_null(
377                            self.last_revision())):
378                    return None
379                return old_tip
380            return None
381
382    def _read_last_revision_info(self):
383        revision_string = self._transport.get_bytes('last-revision')
384        revno, revision_id = revision_string.rstrip(b'\n').split(b' ', 1)
385        revision_id = cache_utf8.get_cached_utf8(revision_id)
386        revno = int(revno)
387        return revno, revision_id
388
389    def _write_last_revision_info(self, revno, revision_id):
390        """Simply write out the revision id, with no checks.
391
392        Use set_last_revision_info to perform this safely.
393
394        Does not update the revision_history cache.
395        """
396        revision_id = _mod_revision.ensure_null(revision_id)
397        out_string = b'%d %s\n' % (revno, revision_id)
398        self._transport.put_bytes('last-revision', out_string,
399                                  mode=self.controldir._get_file_mode())
400
401    def update_feature_flags(self, updated_flags):
402        """Update the feature flags for this branch.
403
404        :param updated_flags: Dictionary mapping feature names to necessities
405            A necessity can be None to indicate the feature should be removed
406        """
407        with self.lock_write():
408            self._format._update_feature_flags(updated_flags)
409            self.control_transport.put_bytes(
410                'format', self._format.as_string())
411
412    def _get_tags_bytes(self):
413        """Get the bytes of a serialised tags dict.
414
415        Note that not all branches support tags, nor do all use the same tags
416        logic: this method is specific to BasicTags. Other tag implementations
417        may use the same method name and behave differently, safely, because
418        of the double-dispatch via
419        format.make_tags->tags_instance->get_tags_dict.
420
421        :return: The bytes of the tags file.
422        :seealso: Branch._set_tags_bytes.
423        """
424        with self.lock_read():
425            if self._tags_bytes is None:
426                self._tags_bytes = self._transport.get_bytes('tags')
427            return self._tags_bytes
428
429    def _set_tags_bytes(self, bytes):
430        """Mirror method for _get_tags_bytes.
431
432        :seealso: Branch._get_tags_bytes.
433        """
434        with self.lock_write():
435            self._tags_bytes = bytes
436            return self._transport.put_bytes('tags', bytes)
437
438    def _clear_cached_state(self):
439        super(BzrBranch, self)._clear_cached_state()
440        self._tags_bytes = None
441
442    def reconcile(self, thorough=True):
443        """Make sure the data stored in this branch is consistent."""
444        from .reconcile import BranchReconciler
445        with self.lock_write():
446            reconciler = BranchReconciler(self, thorough=thorough)
447            return reconciler.reconcile()
448
449    def set_reference_info(self, file_id, branch_location, path=None):
450        """Set the branch location to use for a tree reference."""
451        raise errors.UnsupportedOperation(self.set_reference_info, self)
452
453    def get_reference_info(self, file_id, path=None):
454        """Get the tree_path and branch_location for a tree reference."""
455        raise errors.UnsupportedOperation(self.get_reference_info, self)
456
457    def reference_parent(self, file_id, path, possible_transports=None):
458        """Return the parent branch for a tree-reference.
459
460        :param path: The path of the nested tree in the tree
461        :return: A branch associated with the nested tree
462        """
463        try:
464            branch_location = self.get_reference_info(file_id)[0]
465        except errors.UnsupportedOperation:
466            branch_location = None
467        if branch_location is None:
468            try:
469                return Branch.open_from_transport(
470                    self.controldir.root_transport.clone(path),
471                    possible_transports=possible_transports)
472            except errors.NotBranchError:
473                return None
474        return Branch.open(
475            urlutils.join(
476                urlutils.strip_segment_parameters(self.user_url), branch_location),
477            possible_transports=possible_transports)
478
479
480class BzrBranch8(BzrBranch):
481    """A branch that stores tree-reference locations."""
482
483    def _open_hook(self, possible_transports=None):
484        if self._ignore_fallbacks:
485            return
486        if possible_transports is None:
487            possible_transports = [self.controldir.root_transport]
488        try:
489            url = self.get_stacked_on_url()
490        except (errors.UnstackableRepositoryFormat, errors.NotStacked,
491                UnstackableBranchFormat):
492            pass
493        else:
494            for hook in Branch.hooks['transform_fallback_location']:
495                url = hook(self, url)
496                if url is None:
497                    hook_name = Branch.hooks.get_hook_name(hook)
498                    raise AssertionError(
499                        "'transform_fallback_location' hook %s returned "
500                        "None, not a URL." % hook_name)
501            self._activate_fallback_location(
502                url, possible_transports=possible_transports)
503
504    def __init__(self, *args, **kwargs):
505        self._ignore_fallbacks = kwargs.get('ignore_fallbacks', False)
506        super(BzrBranch8, self).__init__(*args, **kwargs)
507        self._last_revision_info_cache = None
508        self._reference_info = None
509
510    def _clear_cached_state(self):
511        super(BzrBranch8, self)._clear_cached_state()
512        self._last_revision_info_cache = None
513        self._reference_info = None
514
515    def _check_history_violation(self, revision_id):
516        current_revid = self.last_revision()
517        last_revision = _mod_revision.ensure_null(current_revid)
518        if _mod_revision.is_null(last_revision):
519            return
520        graph = self.repository.get_graph()
521        for lh_ancestor in graph.iter_lefthand_ancestry(revision_id):
522            if lh_ancestor == current_revid:
523                return
524        raise errors.AppendRevisionsOnlyViolation(self.user_url)
525
526    def _gen_revision_history(self):
527        """Generate the revision history from last revision
528        """
529        last_revno, last_revision = self.last_revision_info()
530        self._extend_partial_history(stop_index=last_revno - 1)
531        return list(reversed(self._partial_revision_history_cache))
532
533    def _set_parent_location(self, url):
534        """Set the parent branch"""
535        with self.lock_write():
536            self._set_config_location(
537                'parent_location', url, make_relative=True)
538
539    def _get_parent_location(self):
540        """Set the parent branch"""
541        with self.lock_read():
542            return self._get_config_location('parent_location')
543
544    def _set_all_reference_info(self, info_dict):
545        """Replace all reference info stored in a branch.
546
547        :param info_dict: A dict of {file_id: (branch_location, tree_path)}
548        """
549        s = BytesIO()
550        writer = rio.RioWriter(s)
551        for file_id, (branch_location, tree_path) in info_dict.items():
552            stanza = rio.Stanza(file_id=file_id,
553                                branch_location=branch_location)
554            if tree_path is not None:
555                stanza.add('tree_path', tree_path)
556            writer.write_stanza(stanza)
557        with self.lock_write():
558            self._transport.put_bytes('references', s.getvalue())
559            self._reference_info = info_dict
560
561    def _get_all_reference_info(self):
562        """Return all the reference info stored in a branch.
563
564        :return: A dict of {tree_path: (branch_location, file_id)}
565        """
566        with self.lock_read():
567            if self._reference_info is not None:
568                return self._reference_info
569            try:
570                with self._transport.get('references') as rio_file:
571                    stanzas = rio.read_stanzas(rio_file)
572                    info_dict = {
573                        s['file_id'].encode('utf-8'): (
574                            s['branch_location'],
575                            s['tree_path'] if 'tree_path' in s else None)
576                        for s in stanzas}
577            except errors.NoSuchFile:
578                info_dict = {}
579            self._reference_info = info_dict
580            return info_dict
581
582    def set_reference_info(self, file_id, branch_location, tree_path=None):
583        """Set the branch location to use for a tree reference.
584
585        :param branch_location: The location of the branch to retrieve tree
586            references from.
587        :param file_id: The file-id of the tree reference.
588        :param tree_path: The path of the tree reference in the tree.
589        """
590        info_dict = self._get_all_reference_info()
591        info_dict[file_id] = (branch_location, tree_path)
592        if branch_location is None:
593            del info_dict[file_id]
594        self._set_all_reference_info(info_dict)
595
596    def get_reference_info(self, file_id):
597        """Get the tree_path and branch_location for a tree reference.
598
599        :return: a tuple of (branch_location, tree_path)
600        """
601        return self._get_all_reference_info().get(file_id, (None, None))
602
603    def set_push_location(self, location):
604        """See Branch.set_push_location."""
605        self._set_config_location('push_location', location)
606
607    def set_bound_location(self, location):
608        """See Branch.set_push_location."""
609        self._master_branch_cache = None
610        conf = self.get_config_stack()
611        if location is None:
612            if not conf.get('bound'):
613                return False
614            else:
615                conf.set('bound', 'False')
616                return True
617        else:
618            self._set_config_location('bound_location', location,
619                                      config=conf)
620            conf.set('bound', 'True')
621        return True
622
623    def _get_bound_location(self, bound):
624        """Return the bound location in the config file.
625
626        Return None if the bound parameter does not match"""
627        conf = self.get_config_stack()
628        if conf.get('bound') != bound:
629            return None
630        return self._get_config_location('bound_location', config=conf)
631
632    def get_bound_location(self):
633        """See Branch.get_bound_location."""
634        return self._get_bound_location(True)
635
636    def get_old_bound_location(self):
637        """See Branch.get_old_bound_location"""
638        return self._get_bound_location(False)
639
640    def get_stacked_on_url(self):
641        # you can always ask for the URL; but you might not be able to use it
642        # if the repo can't support stacking.
643        # self._check_stackable_repo()
644        # stacked_on_location is only ever defined in branch.conf, so don't
645        # waste effort reading the whole stack of config files.
646        conf = _mod_config.BranchOnlyStack(self)
647        stacked_url = self._get_config_location('stacked_on_location',
648                                                config=conf)
649        if stacked_url is None:
650            raise errors.NotStacked(self)
651        # TODO(jelmer): Clean this up for pad.lv/1696545
652        return stacked_url
653
654    def get_rev_id(self, revno, history=None):
655        """Find the revision id of the specified revno."""
656        if revno == 0:
657            return _mod_revision.NULL_REVISION
658
659        with self.lock_read():
660            last_revno, last_revision_id = self.last_revision_info()
661            if revno <= 0 or revno > last_revno:
662                raise errors.RevnoOutOfBounds(revno, (0, last_revno))
663
664            if history is not None:
665                return history[revno - 1]
666
667            index = last_revno - revno
668            if len(self._partial_revision_history_cache) <= index:
669                self._extend_partial_history(stop_index=index)
670            if len(self._partial_revision_history_cache) > index:
671                return self._partial_revision_history_cache[index]
672            else:
673                raise errors.NoSuchRevision(self, revno)
674
675    def revision_id_to_revno(self, revision_id):
676        """Given a revision id, return its revno"""
677        if _mod_revision.is_null(revision_id):
678            return 0
679        with self.lock_read():
680            try:
681                index = self._partial_revision_history_cache.index(revision_id)
682            except ValueError:
683                try:
684                    self._extend_partial_history(stop_revision=revision_id)
685                except errors.RevisionNotPresent as e:
686                    raise errors.GhostRevisionsHaveNoRevno(
687                        revision_id, e.revision_id)
688                index = len(self._partial_revision_history_cache) - 1
689                if index < 0:
690                    raise errors.NoSuchRevision(self, revision_id)
691                if self._partial_revision_history_cache[index] != revision_id:
692                    raise errors.NoSuchRevision(self, revision_id)
693            return self.revno() - index
694
695
696class BzrBranch7(BzrBranch8):
697    """A branch with support for a fallback repository."""
698
699    def set_reference_info(self, file_id, branch_location, tree_path=None):
700        super(BzrBranch7, self).set_reference_info(
701            file_id, branch_location, tree_path)
702        format_string = BzrBranchFormat8.get_format_string()
703        mutter('Upgrading branch to format %r', format_string)
704        self._transport.put_bytes('format', format_string)
705
706
707class BzrBranch6(BzrBranch7):
708    """See BzrBranchFormat6 for the capabilities of this branch.
709
710    This subclass of BzrBranch7 disables the new features BzrBranch7 added,
711    i.e. stacking.
712    """
713
714    def get_stacked_on_url(self):
715        raise UnstackableBranchFormat(self._format, self.user_url)
716
717
718class BranchFormatMetadir(bzrdir.BzrFormat, BranchFormat):
719    """Base class for branch formats that live in meta directories.
720    """
721
722    def __init__(self):
723        BranchFormat.__init__(self)
724        bzrdir.BzrFormat.__init__(self)
725
726    @classmethod
727    def find_format(klass, controldir, name=None):
728        """Return the format for the branch object in controldir."""
729        try:
730            transport = controldir.get_branch_transport(None, name=name)
731        except errors.NoSuchFile:
732            raise errors.NotBranchError(path=name, controldir=controldir)
733        try:
734            format_string = transport.get_bytes("format")
735        except errors.NoSuchFile:
736            raise errors.NotBranchError(
737                path=transport.base, controldir=controldir)
738        return klass._find_format(format_registry, 'branch', format_string)
739
740    def _branch_class(self):
741        """What class to instantiate on open calls."""
742        raise NotImplementedError(self._branch_class)
743
744    def _get_initial_config(self, append_revisions_only=None):
745        if append_revisions_only:
746            return b"append_revisions_only = True\n"
747        else:
748            # Avoid writing anything if append_revisions_only is disabled,
749            # as that is the default.
750            return b""
751
752    def _initialize_helper(self, a_controldir, utf8_files, name=None,
753                           repository=None):
754        """Initialize a branch in a control dir, with specified files
755
756        :param a_controldir: The bzrdir to initialize the branch in
757        :param utf8_files: The files to create as a list of
758            (filename, content) tuples
759        :param name: Name of colocated branch to create, if any
760        :return: a branch in this format
761        """
762        if name is None:
763            name = a_controldir._get_selected_branch()
764        mutter('creating branch %r in %s', self, a_controldir.user_url)
765        branch_transport = a_controldir.get_branch_transport(self, name=name)
766        control_files = lockable_files.LockableFiles(branch_transport,
767                                                     'lock', lockdir.LockDir)
768        control_files.create_lock()
769        control_files.lock_write()
770        try:
771            utf8_files += [('format', self.as_string())]
772            for (filename, content) in utf8_files:
773                branch_transport.put_bytes(
774                    filename, content,
775                    mode=a_controldir._get_file_mode())
776        finally:
777            control_files.unlock()
778        branch = self.open(a_controldir, name, _found=True,
779                           found_repository=repository)
780        self._run_post_branch_init_hooks(a_controldir, name, branch)
781        return branch
782
783    def open(self, a_controldir, name=None, _found=False,
784             ignore_fallbacks=False, found_repository=None,
785             possible_transports=None):
786        """See BranchFormat.open()."""
787        if name is None:
788            name = a_controldir._get_selected_branch()
789        if not _found:
790            format = BranchFormatMetadir.find_format(a_controldir, name=name)
791            if format.__class__ != self.__class__:
792                raise AssertionError("wrong format %r found for %r" %
793                                     (format, self))
794        transport = a_controldir.get_branch_transport(None, name=name)
795        try:
796            control_files = lockable_files.LockableFiles(transport, 'lock',
797                                                         lockdir.LockDir)
798            if found_repository is None:
799                found_repository = a_controldir.find_repository()
800            return self._branch_class()(
801                _format=self, _control_files=control_files, name=name,
802                a_controldir=a_controldir, _repository=found_repository,
803                ignore_fallbacks=ignore_fallbacks,
804                possible_transports=possible_transports)
805        except errors.NoSuchFile:
806            raise errors.NotBranchError(
807                path=transport.base, controldir=a_controldir)
808
809    @property
810    def _matchingcontroldir(self):
811        ret = bzrdir.BzrDirMetaFormat1()
812        ret.set_branch_format(self)
813        return ret
814
815    def supports_tags(self):
816        return True
817
818    def supports_leaving_lock(self):
819        return True
820
821    def check_support_status(self, allow_unsupported, recommend_upgrade=True,
822                             basedir=None):
823        BranchFormat.check_support_status(
824            self, allow_unsupported=allow_unsupported,
825            recommend_upgrade=recommend_upgrade, basedir=basedir)
826        bzrdir.BzrFormat.check_support_status(
827            self, allow_unsupported=allow_unsupported,
828            recommend_upgrade=recommend_upgrade, basedir=basedir)
829
830
831class BzrBranchFormat6(BranchFormatMetadir):
832    """Branch format with last-revision and tags.
833
834    Unlike previous formats, this has no explicit revision history. Instead,
835    this just stores the last-revision, and the left-hand history leading
836    up to there is the history.
837
838    This format was introduced in bzr 0.15
839    and became the default in 0.91.
840    """
841
842    def _branch_class(self):
843        return BzrBranch6
844
845    @classmethod
846    def get_format_string(cls):
847        """See BranchFormat.get_format_string()."""
848        return b"Bazaar Branch Format 6 (bzr 0.15)\n"
849
850    def get_format_description(self):
851        """See BranchFormat.get_format_description()."""
852        return "Branch format 6"
853
854    def initialize(self, a_controldir, name=None, repository=None,
855                   append_revisions_only=None):
856        """Create a branch of this format in a_controldir."""
857        utf8_files = [
858            ('last-revision', b'0 null:\n'),
859            ('branch.conf', self._get_initial_config(append_revisions_only)),
860            ('tags', b''),
861            ]
862        return self._initialize_helper(
863            a_controldir, utf8_files, name, repository)
864
865    def make_tags(self, branch):
866        """See breezy.branch.BranchFormat.make_tags()."""
867        return _mod_tag.BasicTags(branch)
868
869    def supports_set_append_revisions_only(self):
870        return True
871
872    supports_reference_locations = True
873
874
875class BzrBranchFormat8(BranchFormatMetadir):
876    """Metadir format supporting storing locations of subtree branches."""
877
878    def _branch_class(self):
879        return BzrBranch8
880
881    @classmethod
882    def get_format_string(cls):
883        """See BranchFormat.get_format_string()."""
884        return b"Bazaar Branch Format 8 (needs bzr 1.15)\n"
885
886    def get_format_description(self):
887        """See BranchFormat.get_format_description()."""
888        return "Branch format 8"
889
890    def initialize(self, a_controldir, name=None, repository=None,
891                   append_revisions_only=None):
892        """Create a branch of this format in a_controldir."""
893        utf8_files = [('last-revision', b'0 null:\n'),
894                      ('branch.conf',
895                          self._get_initial_config(append_revisions_only)),
896                      ('tags', b''),
897                      ('references', b'')
898                      ]
899        return self._initialize_helper(
900            a_controldir, utf8_files, name, repository)
901
902    def make_tags(self, branch):
903        """See breezy.branch.BranchFormat.make_tags()."""
904        return _mod_tag.BasicTags(branch)
905
906    def supports_set_append_revisions_only(self):
907        return True
908
909    def supports_stacking(self):
910        return True
911
912    supports_reference_locations = True
913
914
915class BzrBranchFormat7(BranchFormatMetadir):
916    """Branch format with last-revision, tags, and a stacked location pointer.
917
918    The stacked location pointer is passed down to the repository and requires
919    a repository format with supports_external_lookups = True.
920
921    This format was introduced in bzr 1.6.
922    """
923
924    def initialize(self, a_controldir, name=None, repository=None,
925                   append_revisions_only=None):
926        """Create a branch of this format in a_controldir."""
927        utf8_files = [('last-revision', b'0 null:\n'),
928                      ('branch.conf',
929                          self._get_initial_config(append_revisions_only)),
930                      ('tags', b''),
931                      ]
932        return self._initialize_helper(
933            a_controldir, utf8_files, name, repository)
934
935    def _branch_class(self):
936        return BzrBranch7
937
938    @classmethod
939    def get_format_string(cls):
940        """See BranchFormat.get_format_string()."""
941        return b"Bazaar Branch Format 7 (needs bzr 1.6)\n"
942
943    def get_format_description(self):
944        """See BranchFormat.get_format_description()."""
945        return "Branch format 7"
946
947    def supports_set_append_revisions_only(self):
948        return True
949
950    def supports_stacking(self):
951        return True
952
953    def make_tags(self, branch):
954        """See breezy.branch.BranchFormat.make_tags()."""
955        return _mod_tag.BasicTags(branch)
956
957    # This is a white lie; as soon as you set a reference location, we upgrade
958    # you to BzrBranchFormat8.
959    supports_reference_locations = True
960
961
962class BranchReferenceFormat(BranchFormatMetadir):
963    """Bzr branch reference format.
964
965    Branch references are used in implementing checkouts, they
966    act as an alias to the real branch which is at some other url.
967
968    This format has:
969     - A location file
970     - a format string
971    """
972
973    @classmethod
974    def get_format_string(cls):
975        """See BranchFormat.get_format_string()."""
976        return b"Bazaar-NG Branch Reference Format 1\n"
977
978    def get_format_description(self):
979        """See BranchFormat.get_format_description()."""
980        return "Checkout reference format 1"
981
982    def get_reference(self, a_controldir, name=None):
983        """See BranchFormat.get_reference()."""
984        transport = a_controldir.get_branch_transport(None, name=name)
985        url = urlutils.strip_segment_parameters(a_controldir.user_url)
986        return urlutils.join(
987            url, transport.get_bytes('location').decode('utf-8'))
988
989    def _write_reference(self, a_controldir, transport, to_branch):
990        to_url = to_branch.user_url
991        # Ideally, we'd write a relative path here for the benefit of colocated
992        # branches - so that moving a control directory doesn't break
993        # any references to colocated branches. Unfortunately, bzr
994        # does not support relative URLs. See pad.lv/1803845 -- jelmer
995        # to_url = urlutils.relative_url(
996        #    a_controldir.user_url, to_branch.user_url)
997        transport.put_bytes('location', to_url.encode('utf-8'))
998
999    def set_reference(self, a_controldir, name, to_branch):
1000        """See BranchFormat.set_reference()."""
1001        transport = a_controldir.get_branch_transport(None, name=name)
1002        self._write_reference(a_controldir, transport, to_branch)
1003
1004    def initialize(self, a_controldir, name=None, target_branch=None,
1005                   repository=None, append_revisions_only=None):
1006        """Create a branch of this format in a_controldir."""
1007        if target_branch is None:
1008            # this format does not implement branch itself, thus the implicit
1009            # creation contract must see it as uninitializable
1010            raise errors.UninitializableFormat(self)
1011        mutter('creating branch reference in %s', a_controldir.user_url)
1012        if a_controldir._format.fixed_components:
1013            raise errors.IncompatibleFormat(self, a_controldir._format)
1014        if name is None:
1015            name = a_controldir._get_selected_branch()
1016        branch_transport = a_controldir.get_branch_transport(self, name=name)
1017        self._write_reference(a_controldir, branch_transport, target_branch)
1018        branch_transport.put_bytes('format', self.as_string())
1019        branch = self.open(a_controldir, name, _found=True,
1020                           possible_transports=[target_branch.controldir.root_transport])
1021        self._run_post_branch_init_hooks(a_controldir, name, branch)
1022        return branch
1023
1024    def _make_reference_clone_function(format, a_branch):
1025        """Create a clone() routine for a branch dynamically."""
1026        def clone(to_bzrdir, revision_id=None, repository_policy=None, name=None,
1027                  tag_selector=None):
1028            """See Branch.clone()."""
1029            return format.initialize(to_bzrdir, target_branch=a_branch, name=name)
1030            # cannot obey revision_id limits when cloning a reference ...
1031            # FIXME RBC 20060210 either nuke revision_id for clone, or
1032            # emit some sort of warning/error to the caller ?!
1033        return clone
1034
1035    def open(self, a_controldir, name=None, _found=False, location=None,
1036             possible_transports=None, ignore_fallbacks=False,
1037             found_repository=None):
1038        """Return the branch that the branch reference in a_controldir points at.
1039
1040        :param a_controldir: A BzrDir that contains a branch.
1041        :param name: Name of colocated branch to open, if any
1042        :param _found: a private parameter, do not use it. It is used to
1043            indicate if format probing has already be done.
1044        :param ignore_fallbacks: when set, no fallback branches will be opened
1045            (if there are any).  Default is to open fallbacks.
1046        :param location: The location of the referenced branch.  If
1047            unspecified, this will be determined from the branch reference in
1048            a_controldir.
1049        :param possible_transports: An optional reusable transports list.
1050        """
1051        if name is None:
1052            name = a_controldir._get_selected_branch()
1053        if not _found:
1054            format = BranchFormatMetadir.find_format(a_controldir, name=name)
1055            if format.__class__ != self.__class__:
1056                raise AssertionError("wrong format %r found for %r" %
1057                                     (format, self))
1058        if location is None:
1059            location = self.get_reference(a_controldir, name)
1060        real_bzrdir = controldir.ControlDir.open(
1061            location, possible_transports=possible_transports)
1062        result = real_bzrdir.open_branch(
1063            ignore_fallbacks=ignore_fallbacks,
1064            possible_transports=possible_transports)
1065        # this changes the behaviour of result.clone to create a new reference
1066        # rather than a copy of the content of the branch.
1067        # I did not use a proxy object because that needs much more extensive
1068        # testing, and we are only changing one behaviour at the moment.
1069        # If we decide to alter more behaviours - i.e. the implicit nickname
1070        # then this should be refactored to introduce a tested proxy branch
1071        # and a subclass of that for use in overriding clone() and ....
1072        # - RBC 20060210
1073        result.clone = self._make_reference_clone_function(result)
1074        return result
1075
1076
1077class Converter5to6(object):
1078    """Perform an in-place upgrade of format 5 to format 6"""
1079
1080    def convert(self, branch):
1081        # Data for 5 and 6 can peacefully coexist.
1082        format = BzrBranchFormat6()
1083        new_branch = format.open(branch.controldir, _found=True)
1084
1085        # Copy source data into target
1086        new_branch._write_last_revision_info(*branch.last_revision_info())
1087        with new_branch.lock_write():
1088            new_branch.set_parent(branch.get_parent())
1089            new_branch.set_bound_location(branch.get_bound_location())
1090            new_branch.set_push_location(branch.get_push_location())
1091
1092        # New branch has no tags by default
1093        new_branch.tags._set_tag_dict({})
1094
1095        # Copying done; now update target format
1096        new_branch._transport.put_bytes(
1097            'format', format.as_string(),
1098            mode=new_branch.controldir._get_file_mode())
1099
1100        # Clean up old files
1101        new_branch._transport.delete('revision-history')
1102        with branch.lock_write():
1103            try:
1104                branch.set_parent(None)
1105            except errors.NoSuchFile:
1106                pass
1107            branch.set_bound_location(None)
1108
1109
1110class Converter6to7(object):
1111    """Perform an in-place upgrade of format 6 to format 7"""
1112
1113    def convert(self, branch):
1114        format = BzrBranchFormat7()
1115        branch._set_config_location('stacked_on_location', '')
1116        # update target format
1117        branch._transport.put_bytes('format', format.as_string())
1118
1119
1120class Converter7to8(object):
1121    """Perform an in-place upgrade of format 7 to format 8"""
1122
1123    def convert(self, branch):
1124        format = BzrBranchFormat8()
1125        branch._transport.put_bytes('references', b'')
1126        # update target format
1127        branch._transport.put_bytes('format', format.as_string())
1128