~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-08-17 13:33:28 UTC
  • mfrom: (1908.4.15 commit-perf)
  • Revision ID: pqm@pqm.ubuntu.com-20060817133328-ba4026886ad6a107
(jam) small tuning of gzip, more commit benchmarks

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
3
 
 
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
6
5
# the Free Software Foundation; either version 2 of the License, or
7
6
# (at your option) any later version.
8
 
 
 
7
#
9
8
# This program is distributed in the hope that it will be useful,
10
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
11
# GNU General Public License for more details.
13
 
 
 
12
#
14
13
# You should have received a copy of the GNU General Public License
15
14
# along with this program; if not, write to the Free Software
16
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
16
 
18
17
 
19
 
from sets import Set
20
 
 
21
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
22
 
import traceback, socket, fnmatch, difflib, time
23
 
from binascii import hexlify
 
18
from copy import deepcopy
 
19
from cStringIO import StringIO
 
20
from unittest import TestSuite
 
21
from warnings import warn
24
22
 
25
23
import bzrlib
26
 
from inventory import Inventory
27
 
from trace import mutter, note
28
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
29
 
from inventory import InventoryEntry, Inventory
30
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
31
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
32
 
     joinpath, sha_string, file_kind, local_time_offset
33
 
from store import ImmutableStore
34
 
from revision import Revision
35
 
from errors import bailout
36
 
from textui import show_status
37
 
from diff import diff_trees
38
 
 
39
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
40
 
## TODO: Maybe include checks for common corruption of newlines, etc?
41
 
 
42
 
 
43
 
 
 
24
from bzrlib import (
 
25
        bzrdir,
 
26
        cache_utf8,
 
27
        errors,
 
28
        lockdir,
 
29
        osutils,
 
30
        revision,
 
31
        transport,
 
32
        tree,
 
33
        ui,
 
34
        urlutils,
 
35
        )
 
36
from bzrlib.config import TreeConfig
 
37
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
38
import bzrlib.errors as errors
 
39
from bzrlib.errors import (BzrError, BzrCheckError, DivergedBranches, 
 
40
                           HistoryMissing, InvalidRevisionId, 
 
41
                           InvalidRevisionNumber, LockError, NoSuchFile, 
 
42
                           NoSuchRevision, NoWorkingTree, NotVersionedError,
 
43
                           NotBranchError, UninitializableFormat, 
 
44
                           UnlistableStore, UnlistableBranch, 
 
45
                           )
 
46
from bzrlib.lockable_files import LockableFiles, TransportLock
 
47
from bzrlib.symbol_versioning import (deprecated_function,
 
48
                                      deprecated_method,
 
49
                                      DEPRECATED_PARAMETER,
 
50
                                      deprecated_passed,
 
51
                                      zero_eight, zero_nine,
 
52
                                      )
 
53
from bzrlib.trace import mutter, note
 
54
 
 
55
 
 
56
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
57
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
58
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
 
59
 
 
60
 
 
61
# TODO: Maybe include checks for common corruption of newlines, etc?
 
62
 
 
63
# TODO: Some operations like log might retrieve the same revisions
 
64
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
65
# cache in memory to make this faster.  In general anything can be
 
66
# cached in memory between lock and unlock operations. .. nb thats
 
67
# what the transaction identity map provides
44
68
 
45
69
 
46
70
######################################################################
47
71
# branch objects
48
72
 
49
 
class Branch:
 
73
class Branch(object):
50
74
    """Branch holding a history of revisions.
51
75
 
52
 
    :todo: Perhaps use different stores for different classes of object,
53
 
           so that we can keep track of how much space each one uses,
54
 
           or garbage-collect them.
55
 
 
56
 
    :todo: Add a RemoteBranch subclass.  For the basic case of read-only
57
 
           HTTP access this should be very easy by, 
58
 
           just redirecting controlfile access into HTTP requests.
59
 
           We would need a RemoteStore working similarly.
60
 
 
61
 
    :todo: Keep the on-disk branch locked while the object exists.
62
 
 
63
 
    :todo: mkdir() method.
 
76
    base
 
77
        Base directory/url of the branch.
64
78
    """
65
 
    def __init__(self, base, init=False):
66
 
        """Create new branch object at a particular location.
67
 
 
68
 
        :param base: Base directory for the branch.
69
 
 
70
 
        :param init: If True, create new control files in a previously
71
 
             unversioned directory.  If False, the branch must already
72
 
             be versioned.
73
 
 
74
 
        In the test suite, creation of new trees is tested using the
75
 
        `ScratchBranch` class.
76
 
        """
77
 
        self.base = os.path.realpath(base)
78
 
        if init:
79
 
            self._make_control()
 
79
    # this is really an instance variable - FIXME move it there
 
80
    # - RBC 20060112
 
81
    base = None
 
82
 
 
83
    def __init__(self, *ignored, **ignored_too):
 
84
        raise NotImplementedError('The Branch class is abstract')
 
85
 
 
86
    def break_lock(self):
 
87
        """Break a lock if one is present from another instance.
 
88
 
 
89
        Uses the ui factory to ask for confirmation if the lock may be from
 
90
        an active process.
 
91
 
 
92
        This will probe the repository for its lock as well.
 
93
        """
 
94
        self.control_files.break_lock()
 
95
        self.repository.break_lock()
 
96
        master = self.get_master_branch()
 
97
        if master is not None:
 
98
            master.break_lock()
 
99
 
 
100
    @staticmethod
 
101
    @deprecated_method(zero_eight)
 
102
    def open_downlevel(base):
 
103
        """Open a branch which may be of an old format."""
 
104
        return Branch.open(base, _unsupported=True)
 
105
        
 
106
    @staticmethod
 
107
    def open(base, _unsupported=False):
 
108
        """Open the branch rooted at base.
 
109
 
 
110
        For instance, if the branch is at URL/.bzr/branch,
 
111
        Branch.open(URL) -> a Branch instance.
 
112
        """
 
113
        control = bzrdir.BzrDir.open(base, _unsupported)
 
114
        return control.open_branch(_unsupported)
 
115
 
 
116
    @staticmethod
 
117
    def open_containing(url):
 
118
        """Open an existing branch which contains url.
 
119
        
 
120
        This probes for a branch at url, and searches upwards from there.
 
121
 
 
122
        Basically we keep looking up until we find the control directory or
 
123
        run into the root.  If there isn't one, raises NotBranchError.
 
124
        If there is one and it is either an unrecognised format or an unsupported 
 
125
        format, UnknownFormatError or UnsupportedFormatError are raised.
 
126
        If there is one, it is returned, along with the unused portion of url.
 
127
        """
 
128
        control, relpath = bzrdir.BzrDir.open_containing(url)
 
129
        return control.open_branch(), relpath
 
130
 
 
131
    @staticmethod
 
132
    @deprecated_function(zero_eight)
 
133
    def initialize(base):
 
134
        """Create a new working tree and branch, rooted at 'base' (url)
 
135
 
 
136
        NOTE: This will soon be deprecated in favour of creation
 
137
        through a BzrDir.
 
138
        """
 
139
        return bzrdir.BzrDir.create_standalone_workingtree(base).branch
 
140
 
 
141
    def setup_caching(self, cache_root):
 
142
        """Subclasses that care about caching should override this, and set
 
143
        up cached stores located under cache_root.
 
144
        """
 
145
        # seems to be unused, 2006-01-13 mbp
 
146
        warn('%s is deprecated' % self.setup_caching)
 
147
        self.cache_root = cache_root
 
148
 
 
149
    def get_config(self):
 
150
        return bzrlib.config.BranchConfig(self)
 
151
 
 
152
    def _get_nick(self):
 
153
        return self.get_config().get_nickname()
 
154
 
 
155
    def _set_nick(self, nick):
 
156
        self.get_config().set_user_option('nickname', nick)
 
157
 
 
158
    nick = property(_get_nick, _set_nick)
 
159
 
 
160
    def is_locked(self):
 
161
        raise NotImplementedError(self.is_locked)
 
162
 
 
163
    def lock_write(self):
 
164
        raise NotImplementedError(self.lock_write)
 
165
 
 
166
    def lock_read(self):
 
167
        raise NotImplementedError(self.lock_read)
 
168
 
 
169
    def unlock(self):
 
170
        raise NotImplementedError(self.unlock)
 
171
 
 
172
    def peek_lock_mode(self):
 
173
        """Return lock mode for the Branch: 'r', 'w' or None"""
 
174
        raise NotImplementedError(self.peek_lock_mode)
 
175
 
 
176
    def get_physical_lock_status(self):
 
177
        raise NotImplementedError(self.get_physical_lock_status)
 
178
 
 
179
    def abspath(self, name):
 
180
        """Return absolute filename for something in the branch
 
181
        
 
182
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
 
183
        method and not a tree method.
 
184
        """
 
185
        raise NotImplementedError(self.abspath)
 
186
 
 
187
    def bind(self, other):
 
188
        """Bind the local branch the other branch.
 
189
 
 
190
        :param other: The branch to bind to
 
191
        :type other: Branch
 
192
        """
 
193
        raise errors.UpgradeRequired(self.base)
 
194
 
 
195
    @needs_write_lock
 
196
    def fetch(self, from_branch, last_revision=None, pb=None):
 
197
        """Copy revisions from from_branch into this branch.
 
198
 
 
199
        :param from_branch: Where to copy from.
 
200
        :param last_revision: What revision to stop at (None for at the end
 
201
                              of the branch.
 
202
        :param pb: An optional progress bar to use.
 
203
 
 
204
        Returns the copied revision count and the failed revisions in a tuple:
 
205
        (copied, failures).
 
206
        """
 
207
        if self.base == from_branch.base:
 
208
            return (0, [])
 
209
        if pb is None:
 
210
            nested_pb = ui.ui_factory.nested_progress_bar()
 
211
            pb = nested_pb
80
212
        else:
81
 
            if not isdir(self.controlfilename('.')):
82
 
                bailout("not a bzr branch: %s" % quotefn(base),
83
 
                        ['use "bzr init" to initialize a new working tree',
84
 
                         'current bzr can only operate from top-of-tree'])
85
 
            self._check_format()
86
 
 
87
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
88
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
89
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
90
 
 
91
 
 
92
 
    def __str__(self):
93
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
94
 
 
95
 
 
96
 
    __repr__ = __str__
97
 
 
98
 
 
99
 
    def _rel(self, name):
100
 
        """Return filename relative to branch top"""
101
 
        return os.path.join(self.base, name)
102
 
        
103
 
 
104
 
    def controlfilename(self, file_or_path):
105
 
        """Return location relative to branch."""
106
 
        if isinstance(file_or_path, types.StringTypes):
107
 
            file_or_path = [file_or_path]
108
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
109
 
 
110
 
 
111
 
    def controlfile(self, file_or_path, mode='r'):
112
 
        """Open a control file for this branch"""
113
 
        return file(self.controlfilename(file_or_path), mode)
114
 
 
115
 
 
116
 
    def _make_control(self):
117
 
        os.mkdir(self.controlfilename([]))
118
 
        self.controlfile('README', 'w').write(
119
 
            "This is a Bazaar-NG control directory.\n"
120
 
            "Do not change any files in this directory.")
121
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
122
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
123
 
            os.mkdir(self.controlfilename(d))
124
 
        for f in ('revision-history', 'merged-patches',
125
 
                  'pending-merged-patches', 'branch-name'):
126
 
            self.controlfile(f, 'w').write('')
127
 
        mutter('created control directory in ' + self.base)
128
 
        Inventory().write_xml(self.controlfile('inventory','w'))
129
 
 
130
 
 
131
 
    def _check_format(self):
132
 
        """Check this branch format is supported.
133
 
 
134
 
        The current tool only supports the current unstable format.
135
 
 
136
 
        In the future, we might need different in-memory Branch
137
 
        classes to support downlevel branches.  But not yet.
138
 
        """        
139
 
        # read in binary mode to detect newline wierdness.
140
 
        fmt = self.controlfile('branch-format', 'rb').read()
141
 
        if fmt != BZR_BRANCH_FORMAT:
142
 
            bailout('sorry, branch format %r not supported' % fmt,
143
 
                    ['use a different bzr version',
144
 
                     'or remove the .bzr directory and "bzr init" again'])
145
 
 
146
 
 
147
 
    def read_working_inventory(self):
148
 
        """Read the working inventory."""
149
 
        before = time.time()
150
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
151
 
        mutter("loaded inventory of %d items in %f"
152
 
               % (len(inv), time.time() - before))
153
 
        return inv
154
 
 
155
 
 
156
 
    def _write_inventory(self, inv):
157
 
        """Update the working inventory.
158
 
 
159
 
        That is to say, the inventory describing changes underway, that
160
 
        will be committed to the next revision.
161
 
        """
162
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
163
 
        tmpfname = self.controlfilename('inventory.tmp')
164
 
        tmpf = file(tmpfname, 'w')
165
 
        inv.write_xml(tmpf)
166
 
        tmpf.close()
167
 
        os.rename(tmpfname, self.controlfilename('inventory'))
168
 
        mutter('wrote working inventory')
169
 
 
170
 
 
171
 
    inventory = property(read_working_inventory, _write_inventory, None,
172
 
                         """Inventory for the working copy.""")
173
 
 
174
 
 
175
 
    def add(self, files, verbose=False):
176
 
        """Make files versioned.
177
 
 
178
 
        This puts the files in the Added state, so that they will be
179
 
        recorded by the next commit.
180
 
 
181
 
        :todo: Perhaps have an option to add the ids even if the files do
182
 
               not (yet) exist.
183
 
 
184
 
        :todo: Perhaps return the ids of the files?  But then again it
185
 
               is easy to retrieve them if they're needed.
186
 
 
187
 
        :todo: Option to specify file id.
188
 
 
189
 
        :todo: Adding a directory should optionally recurse down and
190
 
               add all non-ignored children.  Perhaps do that in a
191
 
               higher-level method.
192
 
 
193
 
        >>> b = ScratchBranch(files=['foo'])
194
 
        >>> 'foo' in b.unknowns()
195
 
        True
196
 
        >>> b.show_status()
197
 
        ?       foo
198
 
        >>> b.add('foo')
199
 
        >>> 'foo' in b.unknowns()
200
 
        False
201
 
        >>> bool(b.inventory.path2id('foo'))
202
 
        True
203
 
        >>> b.show_status()
204
 
        A       foo
205
 
 
206
 
        >>> b.add('foo')
207
 
        Traceback (most recent call last):
208
 
        ...
209
 
        BzrError: ('foo is already versioned', [])
210
 
 
211
 
        >>> b.add(['nothere'])
212
 
        Traceback (most recent call last):
213
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
214
 
        """
215
 
 
216
 
        # TODO: Re-adding a file that is removed in the working copy
217
 
        # should probably put it back with the previous ID.
218
 
        if isinstance(files, types.StringTypes):
219
 
            files = [files]
220
 
        
221
 
        inv = self.read_working_inventory()
222
 
        for f in files:
223
 
            if is_control_file(f):
224
 
                bailout("cannot add control file %s" % quotefn(f))
225
 
 
226
 
            fp = splitpath(f)
227
 
 
228
 
            if len(fp) == 0:
229
 
                bailout("cannot add top-level %r" % f)
230
 
                
231
 
            fullpath = os.path.normpath(self._rel(f))
232
 
 
233
 
            if isfile(fullpath):
234
 
                kind = 'file'
235
 
            elif isdir(fullpath):
236
 
                kind = 'directory'
237
 
            else:
238
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
239
 
 
240
 
            if len(fp) > 1:
241
 
                parent_name = joinpath(fp[:-1])
242
 
                mutter("lookup parent %r" % parent_name)
243
 
                parent_id = inv.path2id(parent_name)
244
 
                if parent_id == None:
245
 
                    bailout("cannot add: parent %r is not versioned"
246
 
                            % joinpath(fp[:-1]))
247
 
            else:
248
 
                parent_id = None
249
 
 
250
 
            file_id = _gen_file_id(fp[-1])
251
 
            inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
252
 
            if verbose:
253
 
                show_status('A', kind, quotefn(f))
254
 
                
255
 
            mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
256
 
                   % (f, file_id, kind, parent_id))
257
 
        self._write_inventory(inv)
258
 
 
259
 
 
260
 
 
261
 
    def remove(self, files, verbose=False):
262
 
        """Mark nominated files for removal from the inventory.
263
 
 
264
 
        This does not remove their text.  This does not run on 
265
 
 
266
 
        :todo: Refuse to remove modified files unless --force is given?
267
 
 
268
 
        >>> b = ScratchBranch(files=['foo'])
269
 
        >>> b.add('foo')
270
 
        >>> b.inventory.has_filename('foo')
271
 
        True
272
 
        >>> b.remove('foo')
273
 
        >>> b.working_tree().has_filename('foo')
274
 
        True
275
 
        >>> b.inventory.has_filename('foo')
276
 
        False
277
 
        
278
 
        >>> b = ScratchBranch(files=['foo'])
279
 
        >>> b.add('foo')
280
 
        >>> b.commit('one')
281
 
        >>> b.remove('foo')
282
 
        >>> b.commit('two')
283
 
        >>> b.inventory.has_filename('foo') 
284
 
        False
285
 
        >>> b.basis_tree().has_filename('foo') 
286
 
        False
287
 
        >>> b.working_tree().has_filename('foo') 
288
 
        True
289
 
 
290
 
        :todo: Do something useful with directories.
291
 
 
292
 
        :todo: Should this remove the text or not?  Tough call; not
293
 
        removing may be useful and the user can just use use rm, and
294
 
        is the opposite of add.  Removing it is consistent with most
295
 
        other tools.  Maybe an option.
296
 
        """
297
 
        ## TODO: Normalize names
298
 
        ## TODO: Remove nested loops; better scalability
299
 
 
300
 
        if isinstance(files, types.StringTypes):
301
 
            files = [files]
302
 
        
303
 
        inv = self.read_working_inventory()
304
 
 
305
 
        # do this before any modifications
306
 
        for f in files:
307
 
            fid = inv.path2id(f)
308
 
            if not fid:
309
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
310
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
311
 
            if verbose:
312
 
                show_status('D', inv[fid].kind, quotefn(f))
313
 
            del inv[fid]
314
 
 
315
 
        self._write_inventory(inv)
316
 
 
317
 
 
318
 
    def unknowns(self):
319
 
        """Return all unknown files.
320
 
 
321
 
        These are files in the working directory that are not versioned or
322
 
        control files or ignored.
323
 
        
324
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
325
 
        >>> list(b.unknowns())
326
 
        ['foo']
327
 
        >>> b.add('foo')
328
 
        >>> list(b.unknowns())
329
 
        []
330
 
        >>> b.remove('foo')
331
 
        >>> list(b.unknowns())
332
 
        ['foo']
333
 
        """
334
 
        return self.working_tree().unknowns()
335
 
 
336
 
 
337
 
    def commit(self, message, timestamp=None, timezone=None,
338
 
               committer=None,
339
 
               verbose=False):
340
 
        """Commit working copy as a new revision.
341
 
        
342
 
        The basic approach is to add all the file texts into the
343
 
        store, then the inventory, then make a new revision pointing
344
 
        to that inventory and store that.
345
 
        
346
 
        This is not quite safe if the working copy changes during the
347
 
        commit; for the moment that is simply not allowed.  A better
348
 
        approach is to make a temporary copy of the files before
349
 
        computing their hashes, and then add those hashes in turn to
350
 
        the inventory.  This should mean at least that there are no
351
 
        broken hash pointers.  There is no way we can get a snapshot
352
 
        of the whole directory at an instant.  This would also have to
353
 
        be robust against files disappearing, moving, etc.  So the
354
 
        whole thing is a bit hard.
355
 
 
356
 
        :param timestamp: if not None, seconds-since-epoch for a
357
 
             postdated/predated commit.
358
 
        """
359
 
 
360
 
        ## TODO: Show branch names
361
 
 
362
 
        # TODO: Don't commit if there are no changes, unless forced?
363
 
 
364
 
        # First walk over the working inventory; and both update that
365
 
        # and also build a new revision inventory.  The revision
366
 
        # inventory needs to hold the text-id, sha1 and size of the
367
 
        # actual file versions committed in the revision.  (These are
368
 
        # not present in the working inventory.)  We also need to
369
 
        # detect missing/deleted files, and remove them from the
370
 
        # working inventory.
371
 
 
372
 
        work_inv = self.read_working_inventory()
373
 
        inv = Inventory()
374
 
        basis = self.basis_tree()
375
 
        basis_inv = basis.inventory
376
 
        missing_ids = []
377
 
        for path, entry in work_inv.iter_entries():
378
 
            ## TODO: Cope with files that have gone missing.
379
 
 
380
 
            ## TODO: Check that the file kind has not changed from the previous
381
 
            ## revision of this file (if any).
382
 
 
383
 
            entry = entry.copy()
384
 
 
385
 
            p = self._rel(path)
386
 
            file_id = entry.file_id
387
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
388
 
 
389
 
            if not os.path.exists(p):
390
 
                mutter("    file is missing, removing from inventory")
391
 
                if verbose:
392
 
                    show_status('D', entry.kind, quotefn(path))
393
 
                missing_ids.append(file_id)
394
 
                continue
395
 
 
396
 
            # TODO: Handle files that have been deleted
397
 
 
398
 
            # TODO: Maybe a special case for empty files?  Seems a
399
 
            # waste to store them many times.
400
 
 
401
 
            inv.add(entry)
402
 
 
403
 
            if basis_inv.has_id(file_id):
404
 
                old_kind = basis_inv[file_id].kind
405
 
                if old_kind != entry.kind:
406
 
                    bailout("entry %r changed kind from %r to %r"
407
 
                            % (file_id, old_kind, entry.kind))
408
 
 
409
 
            if entry.kind == 'directory':
410
 
                if not isdir(p):
411
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
412
 
            elif entry.kind == 'file':
413
 
                if not isfile(p):
414
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
415
 
 
416
 
                content = file(p, 'rb').read()
417
 
 
418
 
                entry.text_sha1 = sha_string(content)
419
 
                entry.text_size = len(content)
420
 
 
421
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
422
 
                if (old_ie
423
 
                    and (old_ie.text_size == entry.text_size)
424
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
425
 
                    ## assert content == basis.get_file(file_id).read()
426
 
                    entry.text_id = basis_inv[file_id].text_id
427
 
                    mutter('    unchanged from previous text_id {%s}' %
428
 
                           entry.text_id)
429
 
                    
 
213
            nested_pb = None
 
214
 
 
215
        from_branch.lock_read()
 
216
        try:
 
217
            if last_revision is None:
 
218
                pb.update('get source history')
 
219
                from_history = from_branch.revision_history()
 
220
                if from_history:
 
221
                    last_revision = from_history[-1]
430
222
                else:
431
 
                    entry.text_id = _gen_file_id(entry.name)
432
 
                    self.text_store.add(content, entry.text_id)
433
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
434
 
                    if verbose:
435
 
                        if not old_ie:
436
 
                            state = 'A'
437
 
                        elif (old_ie.name == entry.name
438
 
                              and old_ie.parent_id == entry.parent_id):
439
 
                            state = 'R'
440
 
                        else:
441
 
                            state = 'M'
442
 
 
443
 
                        show_status(state, entry.kind, quotefn(path))
444
 
 
445
 
        for file_id in missing_ids:
446
 
            # have to do this later so we don't mess up the iterator.
447
 
            # since parents may be removed before their children we
448
 
            # have to test.
449
 
 
450
 
            # FIXME: There's probably a better way to do this; perhaps
451
 
            # the workingtree should know how to filter itself.
452
 
            if work_inv.has_id(file_id):
453
 
                del work_inv[file_id]
454
 
 
455
 
 
456
 
        inv_id = rev_id = _gen_revision_id(time.time())
457
 
        
458
 
        inv_tmp = tempfile.TemporaryFile()
459
 
        inv.write_xml(inv_tmp)
460
 
        inv_tmp.seek(0)
461
 
        self.inventory_store.add(inv_tmp, inv_id)
462
 
        mutter('new inventory_id is {%s}' % inv_id)
463
 
 
464
 
        self._write_inventory(work_inv)
465
 
 
466
 
        if timestamp == None:
467
 
            timestamp = time.time()
468
 
 
469
 
        if committer == None:
470
 
            committer = username()
471
 
 
472
 
        if timezone == None:
473
 
            timezone = local_time_offset()
474
 
 
475
 
        mutter("building commit log message")
476
 
        rev = Revision(timestamp=timestamp,
477
 
                       timezone=timezone,
478
 
                       committer=committer,
479
 
                       precursor = self.last_patch(),
480
 
                       message = message,
481
 
                       inventory_id=inv_id,
482
 
                       revision_id=rev_id)
483
 
 
484
 
        rev_tmp = tempfile.TemporaryFile()
485
 
        rev.write_xml(rev_tmp)
486
 
        rev_tmp.seek(0)
487
 
        self.revision_store.add(rev_tmp, rev_id)
488
 
        mutter("new revision_id is {%s}" % rev_id)
489
 
        
490
 
        ## XXX: Everything up to here can simply be orphaned if we abort
491
 
        ## the commit; it will leave junk files behind but that doesn't
492
 
        ## matter.
493
 
 
494
 
        ## TODO: Read back the just-generated changeset, and make sure it
495
 
        ## applies and recreates the right state.
496
 
 
497
 
        ## TODO: Also calculate and store the inventory SHA1
498
 
        mutter("committing patch r%d" % (self.revno() + 1))
499
 
 
500
 
        mutter("append to revision-history")
501
 
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
502
 
 
503
 
        mutter("done!")
504
 
 
505
 
 
506
 
    def get_revision(self, revision_id):
507
 
        """Return the Revision object for a named revision"""
508
 
        r = Revision.read_xml(self.revision_store[revision_id])
509
 
        assert r.revision_id == revision_id
510
 
        return r
511
 
 
512
 
 
513
 
    def get_inventory(self, inventory_id):
514
 
        """Get Inventory object by hash.
515
 
 
516
 
        :todo: Perhaps for this and similar methods, take a revision
517
 
               parameter which can be either an integer revno or a
518
 
               string hash."""
519
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
520
 
        return i
521
 
 
522
 
 
523
 
    def get_revision_inventory(self, revision_id):
524
 
        """Return inventory of a past revision."""
525
 
        if revision_id == None:
526
 
            return Inventory()
527
 
        else:
528
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
529
 
 
 
223
                    # no history in the source branch
 
224
                    last_revision = revision.NULL_REVISION
 
225
            return self.repository.fetch(from_branch.repository,
 
226
                                         revision_id=last_revision,
 
227
                                         pb=nested_pb)
 
228
        finally:
 
229
            if nested_pb is not None:
 
230
                nested_pb.finished()
 
231
            from_branch.unlock()
 
232
 
 
233
    def get_bound_location(self):
 
234
        """Return the URL of the branch we are bound to.
 
235
 
 
236
        Older format branches cannot bind, please be sure to use a metadir
 
237
        branch.
 
238
        """
 
239
        return None
 
240
    
 
241
    def get_commit_builder(self, parents, config=None, timestamp=None, 
 
242
                           timezone=None, committer=None, revprops=None, 
 
243
                           revision_id=None):
 
244
        """Obtain a CommitBuilder for this branch.
 
245
        
 
246
        :param parents: Revision ids of the parents of the new revision.
 
247
        :param config: Optional configuration to use.
 
248
        :param timestamp: Optional timestamp recorded for commit.
 
249
        :param timezone: Optional timezone for timestamp.
 
250
        :param committer: Optional committer to set for commit.
 
251
        :param revprops: Optional dictionary of revision properties.
 
252
        :param revision_id: Optional revision id.
 
253
        """
 
254
 
 
255
        if config is None:
 
256
            config = self.get_config()
 
257
        
 
258
        return self.repository.get_commit_builder(self, parents, config, 
 
259
            timestamp, timezone, committer, revprops, revision_id)
 
260
 
 
261
    def get_master_branch(self):
 
262
        """Return the branch we are bound to.
 
263
        
 
264
        :return: Either a Branch, or None
 
265
        """
 
266
        return None
 
267
 
 
268
    def get_revision_delta(self, revno):
 
269
        """Return the delta for one revision.
 
270
 
 
271
        The delta is relative to its mainline predecessor, or the
 
272
        empty tree for revision 1.
 
273
        """
 
274
        assert isinstance(revno, int)
 
275
        rh = self.revision_history()
 
276
        if not (1 <= revno <= len(rh)):
 
277
            raise InvalidRevisionNumber(revno)
 
278
        return self.repository.get_revision_delta(rh[revno-1])
 
279
 
 
280
    def get_root_id(self):
 
281
        """Return the id of this branches root"""
 
282
        raise NotImplementedError(self.get_root_id)
 
283
 
 
284
    def print_file(self, file, revision_id):
 
285
        """Print `file` to stdout."""
 
286
        raise NotImplementedError(self.print_file)
 
287
 
 
288
    def append_revision(self, *revision_ids):
 
289
        raise NotImplementedError(self.append_revision)
 
290
 
 
291
    def set_revision_history(self, rev_history):
 
292
        raise NotImplementedError(self.set_revision_history)
530
293
 
531
294
    def revision_history(self):
532
 
        """Return sequence of revision hashes on to this branch.
533
 
 
534
 
        >>> ScratchBranch().revision_history()
535
 
        []
536
 
        """
537
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
538
 
 
 
295
        """Return sequence of revision hashes on to this branch."""
 
296
        raise NotImplementedError('revision_history is abstract')
539
297
 
540
298
    def revno(self):
541
299
        """Return current revision number for this branch.
542
300
 
543
301
        That is equivalent to the number of revisions committed to
544
302
        this branch.
545
 
 
546
 
        >>> b = ScratchBranch()
547
 
        >>> b.revno()
548
 
        0
549
 
        >>> b.commit('no foo')
550
 
        >>> b.revno()
551
 
        1
552
303
        """
553
304
        return len(self.revision_history())
554
305
 
555
 
 
556
 
    def last_patch(self):
557
 
        """Return last patch hash, or None if no history.
558
 
 
559
 
        >>> ScratchBranch().last_patch() == None
560
 
        True
561
 
        """
 
306
    def unbind(self):
 
307
        """Older format branches cannot bind or unbind."""
 
308
        raise errors.UpgradeRequired(self.base)
 
309
 
 
310
    def last_revision(self):
 
311
        """Return last revision id, or None"""
562
312
        ph = self.revision_history()
563
313
        if ph:
564
314
            return ph[-1]
565
 
 
566
 
 
567
 
    def lookup_revision(self, revno):
568
 
        """Return revision hash for revision number."""
 
315
        else:
 
316
            return None
 
317
 
 
318
    def missing_revisions(self, other, stop_revision=None):
 
319
        """Return a list of new revisions that would perfectly fit.
 
320
        
 
321
        If self and other have not diverged, return a list of the revisions
 
322
        present in other, but missing from self.
 
323
        """
 
324
        self_history = self.revision_history()
 
325
        self_len = len(self_history)
 
326
        other_history = other.revision_history()
 
327
        other_len = len(other_history)
 
328
        common_index = min(self_len, other_len) -1
 
329
        if common_index >= 0 and \
 
330
            self_history[common_index] != other_history[common_index]:
 
331
            raise DivergedBranches(self, other)
 
332
 
 
333
        if stop_revision is None:
 
334
            stop_revision = other_len
 
335
        else:
 
336
            assert isinstance(stop_revision, int)
 
337
            if stop_revision > other_len:
 
338
                raise errors.NoSuchRevision(self, stop_revision)
 
339
        return other_history[self_len:stop_revision]
 
340
 
 
341
    def update_revisions(self, other, stop_revision=None):
 
342
        """Pull in new perfect-fit revisions.
 
343
 
 
344
        :param other: Another Branch to pull from
 
345
        :param stop_revision: Updated until the given revision
 
346
        :return: None
 
347
        """
 
348
        raise NotImplementedError(self.update_revisions)
 
349
 
 
350
    def revision_id_to_revno(self, revision_id):
 
351
        """Given a revision id, return its revno"""
 
352
        if revision_id is None:
 
353
            return 0
 
354
        history = self.revision_history()
 
355
        try:
 
356
            return history.index(revision_id) + 1
 
357
        except ValueError:
 
358
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
359
 
 
360
    def get_rev_id(self, revno, history=None):
 
361
        """Find the revision id of the specified revno."""
569
362
        if revno == 0:
570
363
            return None
571
 
 
572
 
        try:
573
 
            # list is 0-based; revisions are 1-based
574
 
            return self.revision_history()[revno-1]
575
 
        except IndexError:
576
 
            bailout("no such revision %s" % revno)
577
 
 
578
 
 
579
 
    def revision_tree(self, revision_id):
580
 
        """Return Tree for a revision on this branch.
581
 
 
582
 
        `revision_id` may be None for the null revision, in which case
583
 
        an `EmptyTree` is returned."""
584
 
 
585
 
        if revision_id == None:
586
 
            return EmptyTree()
587
 
        else:
588
 
            inv = self.get_revision_inventory(revision_id)
589
 
            return RevisionTree(self.text_store, inv)
590
 
 
591
 
 
 
364
        if history is None:
 
365
            history = self.revision_history()
 
366
        elif revno <= 0 or revno > len(history):
 
367
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
368
        return history[revno - 1]
 
369
 
 
370
    def pull(self, source, overwrite=False, stop_revision=None):
 
371
        raise NotImplementedError(self.pull)
 
372
 
 
373
    def basis_tree(self):
 
374
        """Return `Tree` object for last revision."""
 
375
        return self.repository.revision_tree(self.last_revision())
 
376
 
 
377
    def rename_one(self, from_rel, to_rel):
 
378
        """Rename one file.
 
379
 
 
380
        This can change the directory or the filename or both.
 
381
        """
 
382
        raise NotImplementedError(self.rename_one)
 
383
 
 
384
    def move(self, from_paths, to_name):
 
385
        """Rename files.
 
386
 
 
387
        to_name must exist as a versioned directory.
 
388
 
 
389
        If to_name exists and is a directory, the files are moved into
 
390
        it, keeping their old names.  If it is a directory, 
 
391
 
 
392
        Note that to_name is only the last component of the new name;
 
393
        this doesn't change the directory.
 
394
 
 
395
        This returns a list of (from_path, to_path) pairs for each
 
396
        entry that is moved.
 
397
        """
 
398
        raise NotImplementedError(self.move)
 
399
 
 
400
    def get_parent(self):
 
401
        """Return the parent location of the branch.
 
402
 
 
403
        This is the default location for push/pull/missing.  The usual
 
404
        pattern is that the user can override it by specifying a
 
405
        location.
 
406
        """
 
407
        raise NotImplementedError(self.get_parent)
 
408
 
 
409
    def get_submit_branch(self):
 
410
        """Return the submit location of the branch.
 
411
 
 
412
        This is the default location for bundle.  The usual
 
413
        pattern is that the user can override it by specifying a
 
414
        location.
 
415
        """
 
416
        return self.get_config().get_user_option('submit_branch')
 
417
 
 
418
    def set_submit_branch(self, location):
 
419
        """Return the submit location of the branch.
 
420
 
 
421
        This is the default location for bundle.  The usual
 
422
        pattern is that the user can override it by specifying a
 
423
        location.
 
424
        """
 
425
        self.get_config().set_user_option('submit_branch', location)
 
426
 
 
427
    def get_push_location(self):
 
428
        """Return the None or the location to push this branch to."""
 
429
        raise NotImplementedError(self.get_push_location)
 
430
 
 
431
    def set_push_location(self, location):
 
432
        """Set a new push location for this branch."""
 
433
        raise NotImplementedError(self.set_push_location)
 
434
 
 
435
    def set_parent(self, url):
 
436
        raise NotImplementedError(self.set_parent)
 
437
 
 
438
    @needs_write_lock
 
439
    def update(self):
 
440
        """Synchronise this branch with the master branch if any. 
 
441
 
 
442
        :return: None or the last_revision pivoted out during the update.
 
443
        """
 
444
        return None
 
445
 
 
446
    def check_revno(self, revno):
 
447
        """\
 
448
        Check whether a revno corresponds to any revision.
 
449
        Zero (the NULL revision) is considered valid.
 
450
        """
 
451
        if revno != 0:
 
452
            self.check_real_revno(revno)
 
453
            
 
454
    def check_real_revno(self, revno):
 
455
        """\
 
456
        Check whether a revno corresponds to a real revision.
 
457
        Zero (the NULL revision) is considered invalid
 
458
        """
 
459
        if revno < 1 or revno > self.revno():
 
460
            raise InvalidRevisionNumber(revno)
 
461
 
 
462
    @needs_read_lock
 
463
    def clone(self, *args, **kwargs):
 
464
        """Clone this branch into to_bzrdir preserving all semantic values.
 
465
        
 
466
        revision_id: if not None, the revision history in the new branch will
 
467
                     be truncated to end with revision_id.
 
468
        """
 
469
        # for API compatibility, until 0.8 releases we provide the old api:
 
470
        # def clone(self, to_location, revision=None, basis_branch=None, to_branch_format=None):
 
471
        # after 0.8 releases, the *args and **kwargs should be changed:
 
472
        # def clone(self, to_bzrdir, revision_id=None):
 
473
        if (kwargs.get('to_location', None) or
 
474
            kwargs.get('revision', None) or
 
475
            kwargs.get('basis_branch', None) or
 
476
            (len(args) and isinstance(args[0], basestring))):
 
477
            # backwards compatibility api:
 
478
            warn("Branch.clone() has been deprecated for BzrDir.clone() from"
 
479
                 " bzrlib 0.8.", DeprecationWarning, stacklevel=3)
 
480
            # get basis_branch
 
481
            if len(args) > 2:
 
482
                basis_branch = args[2]
 
483
            else:
 
484
                basis_branch = kwargs.get('basis_branch', None)
 
485
            if basis_branch:
 
486
                basis = basis_branch.bzrdir
 
487
            else:
 
488
                basis = None
 
489
            # get revision
 
490
            if len(args) > 1:
 
491
                revision_id = args[1]
 
492
            else:
 
493
                revision_id = kwargs.get('revision', None)
 
494
            # get location
 
495
            if len(args):
 
496
                url = args[0]
 
497
            else:
 
498
                # no default to raise if not provided.
 
499
                url = kwargs.get('to_location')
 
500
            return self.bzrdir.clone(url,
 
501
                                     revision_id=revision_id,
 
502
                                     basis=basis).open_branch()
 
503
        # new cleaner api.
 
504
        # generate args by hand 
 
505
        if len(args) > 1:
 
506
            revision_id = args[1]
 
507
        else:
 
508
            revision_id = kwargs.get('revision_id', None)
 
509
        if len(args):
 
510
            to_bzrdir = args[0]
 
511
        else:
 
512
            # no default to raise if not provided.
 
513
            to_bzrdir = kwargs.get('to_bzrdir')
 
514
        result = self._format.initialize(to_bzrdir)
 
515
        self.copy_content_into(result, revision_id=revision_id)
 
516
        return  result
 
517
 
 
518
    @needs_read_lock
 
519
    def sprout(self, to_bzrdir, revision_id=None):
 
520
        """Create a new line of development from the branch, into to_bzrdir.
 
521
        
 
522
        revision_id: if not None, the revision history in the new branch will
 
523
                     be truncated to end with revision_id.
 
524
        """
 
525
        result = self._format.initialize(to_bzrdir)
 
526
        self.copy_content_into(result, revision_id=revision_id)
 
527
        result.set_parent(self.bzrdir.root_transport.base)
 
528
        return result
 
529
 
 
530
    @needs_read_lock
 
531
    def copy_content_into(self, destination, revision_id=None):
 
532
        """Copy the content of self into destination.
 
533
 
 
534
        revision_id: if not None, the revision history in the new branch will
 
535
                     be truncated to end with revision_id.
 
536
        """
 
537
        new_history = self.revision_history()
 
538
        if revision_id is not None:
 
539
            try:
 
540
                new_history = new_history[:new_history.index(revision_id) + 1]
 
541
            except ValueError:
 
542
                rev = self.repository.get_revision(revision_id)
 
543
                new_history = rev.get_history(self.repository)[1:]
 
544
        destination.set_revision_history(new_history)
 
545
        try:
 
546
            parent = self.get_parent()
 
547
        except errors.InaccessibleParent, e:
 
548
            mutter('parent was not accessible to copy: %s', e)
 
549
        else:
 
550
            if parent:
 
551
                destination.set_parent(parent)
 
552
 
 
553
    @needs_read_lock
 
554
    def check(self):
 
555
        """Check consistency of the branch.
 
556
 
 
557
        In particular this checks that revisions given in the revision-history
 
558
        do actually match up in the revision graph, and that they're all 
 
559
        present in the repository.
 
560
        
 
561
        Callers will typically also want to check the repository.
 
562
 
 
563
        :return: A BranchCheckResult.
 
564
        """
 
565
        mainline_parent_id = None
 
566
        for revision_id in self.revision_history():
 
567
            try:
 
568
                revision = self.repository.get_revision(revision_id)
 
569
            except errors.NoSuchRevision, e:
 
570
                raise errors.BzrCheckError("mainline revision {%s} not in repository"
 
571
                            % revision_id)
 
572
            # In general the first entry on the revision history has no parents.
 
573
            # But it's not illegal for it to have parents listed; this can happen
 
574
            # in imports from Arch when the parents weren't reachable.
 
575
            if mainline_parent_id is not None:
 
576
                if mainline_parent_id not in revision.parent_ids:
 
577
                    raise errors.BzrCheckError("previous revision {%s} not listed among "
 
578
                                        "parents of {%s}"
 
579
                                        % (mainline_parent_id, revision_id))
 
580
            mainline_parent_id = revision_id
 
581
        return BranchCheckResult(self)
 
582
 
 
583
    def create_checkout(self, to_location, revision_id=None, 
 
584
                        lightweight=False):
 
585
        """Create a checkout of a branch.
 
586
        
 
587
        :param to_location: The url to produce the checkout at
 
588
        :param revision_id: The revision to check out
 
589
        :param lightweight: If True, produce a lightweight checkout, otherwise,
 
590
        produce a bound branch (heavyweight checkout)
 
591
        :return: The tree of the created checkout
 
592
        """
 
593
        if lightweight:
 
594
            t = transport.get_transport(to_location)
 
595
            try:
 
596
                t.mkdir('.')
 
597
            except errors.FileExists:
 
598
                pass
 
599
            checkout = bzrdir.BzrDirMetaFormat1().initialize_on_transport(t)
 
600
            BranchReferenceFormat().initialize(checkout, self)
 
601
        else:
 
602
            checkout_branch = bzrdir.BzrDir.create_branch_convenience(
 
603
                to_location, force_new_tree=False)
 
604
            checkout = checkout_branch.bzrdir
 
605
            checkout_branch.bind(self)
 
606
            if revision_id is not None:
 
607
                rh = checkout_branch.revision_history()
 
608
                new_rh = rh[:rh.index(revision_id) + 1]
 
609
                checkout_branch.set_revision_history(new_rh)
 
610
        return checkout.create_workingtree(revision_id)
 
611
 
 
612
 
 
613
class BranchFormat(object):
 
614
    """An encapsulation of the initialization and open routines for a format.
 
615
 
 
616
    Formats provide three things:
 
617
     * An initialization routine,
 
618
     * a format string,
 
619
     * an open routine.
 
620
 
 
621
    Formats are placed in an dict by their format string for reference 
 
622
    during branch opening. Its not required that these be instances, they
 
623
    can be classes themselves with class methods - it simply depends on 
 
624
    whether state is needed for a given format or not.
 
625
 
 
626
    Once a format is deprecated, just deprecate the initialize and open
 
627
    methods on the format class. Do not deprecate the object, as the 
 
628
    object will be created every time regardless.
 
629
    """
 
630
 
 
631
    _default_format = None
 
632
    """The default format used for new branches."""
 
633
 
 
634
    _formats = {}
 
635
    """The known formats."""
 
636
 
 
637
    @classmethod
 
638
    def find_format(klass, a_bzrdir):
 
639
        """Return the format for the branch object in a_bzrdir."""
 
640
        try:
 
641
            transport = a_bzrdir.get_branch_transport(None)
 
642
            format_string = transport.get("format").read()
 
643
            return klass._formats[format_string]
 
644
        except NoSuchFile:
 
645
            raise NotBranchError(path=transport.base)
 
646
        except KeyError:
 
647
            raise errors.UnknownFormatError(format=format_string)
 
648
 
 
649
    @classmethod
 
650
    def get_default_format(klass):
 
651
        """Return the current default format."""
 
652
        return klass._default_format
 
653
 
 
654
    def get_format_string(self):
 
655
        """Return the ASCII format string that identifies this format."""
 
656
        raise NotImplementedError(self.get_format_string)
 
657
 
 
658
    def get_format_description(self):
 
659
        """Return the short format description for this format."""
 
660
        raise NotImplementedError(self.get_format_string)
 
661
 
 
662
    def initialize(self, a_bzrdir):
 
663
        """Create a branch of this format in a_bzrdir."""
 
664
        raise NotImplementedError(self.initialize)
 
665
 
 
666
    def is_supported(self):
 
667
        """Is this format supported?
 
668
 
 
669
        Supported formats can be initialized and opened.
 
670
        Unsupported formats may not support initialization or committing or 
 
671
        some other features depending on the reason for not being supported.
 
672
        """
 
673
        return True
 
674
 
 
675
    def open(self, a_bzrdir, _found=False):
 
676
        """Return the branch object for a_bzrdir
 
677
 
 
678
        _found is a private parameter, do not use it. It is used to indicate
 
679
               if format probing has already be done.
 
680
        """
 
681
        raise NotImplementedError(self.open)
 
682
 
 
683
    @classmethod
 
684
    def register_format(klass, format):
 
685
        klass._formats[format.get_format_string()] = format
 
686
 
 
687
    @classmethod
 
688
    def set_default_format(klass, format):
 
689
        klass._default_format = format
 
690
 
 
691
    @classmethod
 
692
    def unregister_format(klass, format):
 
693
        assert klass._formats[format.get_format_string()] is format
 
694
        del klass._formats[format.get_format_string()]
 
695
 
 
696
    def __str__(self):
 
697
        return self.get_format_string().rstrip()
 
698
 
 
699
 
 
700
class BzrBranchFormat4(BranchFormat):
 
701
    """Bzr branch format 4.
 
702
 
 
703
    This format has:
 
704
     - a revision-history file.
 
705
     - a branch-lock lock file [ to be shared with the bzrdir ]
 
706
    """
 
707
 
 
708
    def get_format_description(self):
 
709
        """See BranchFormat.get_format_description()."""
 
710
        return "Branch format 4"
 
711
 
 
712
    def initialize(self, a_bzrdir):
 
713
        """Create a branch of this format in a_bzrdir."""
 
714
        mutter('creating branch in %s', a_bzrdir.transport.base)
 
715
        branch_transport = a_bzrdir.get_branch_transport(self)
 
716
        utf8_files = [('revision-history', ''),
 
717
                      ('branch-name', ''),
 
718
                      ]
 
719
        control_files = LockableFiles(branch_transport, 'branch-lock',
 
720
                                      TransportLock)
 
721
        control_files.create_lock()
 
722
        control_files.lock_write()
 
723
        try:
 
724
            for file, content in utf8_files:
 
725
                control_files.put_utf8(file, content)
 
726
        finally:
 
727
            control_files.unlock()
 
728
        return self.open(a_bzrdir, _found=True)
 
729
 
 
730
    def __init__(self):
 
731
        super(BzrBranchFormat4, self).__init__()
 
732
        self._matchingbzrdir = bzrdir.BzrDirFormat6()
 
733
 
 
734
    def open(self, a_bzrdir, _found=False):
 
735
        """Return the branch object for a_bzrdir
 
736
 
 
737
        _found is a private parameter, do not use it. It is used to indicate
 
738
               if format probing has already be done.
 
739
        """
 
740
        if not _found:
 
741
            # we are being called directly and must probe.
 
742
            raise NotImplementedError
 
743
        return BzrBranch(_format=self,
 
744
                         _control_files=a_bzrdir._control_files,
 
745
                         a_bzrdir=a_bzrdir,
 
746
                         _repository=a_bzrdir.open_repository())
 
747
 
 
748
    def __str__(self):
 
749
        return "Bazaar-NG branch format 4"
 
750
 
 
751
 
 
752
class BzrBranchFormat5(BranchFormat):
 
753
    """Bzr branch format 5.
 
754
 
 
755
    This format has:
 
756
     - a revision-history file.
 
757
     - a format string
 
758
     - a lock dir guarding the branch itself
 
759
     - all of this stored in a branch/ subdirectory
 
760
     - works with shared repositories.
 
761
 
 
762
    This format is new in bzr 0.8.
 
763
    """
 
764
 
 
765
    def get_format_string(self):
 
766
        """See BranchFormat.get_format_string()."""
 
767
        return "Bazaar-NG branch format 5\n"
 
768
 
 
769
    def get_format_description(self):
 
770
        """See BranchFormat.get_format_description()."""
 
771
        return "Branch format 5"
 
772
        
 
773
    def initialize(self, a_bzrdir):
 
774
        """Create a branch of this format in a_bzrdir."""
 
775
        mutter('creating branch %r in %s', self, a_bzrdir.transport.base)
 
776
        branch_transport = a_bzrdir.get_branch_transport(self)
 
777
        utf8_files = [('revision-history', ''),
 
778
                      ('branch-name', ''),
 
779
                      ]
 
780
        control_files = LockableFiles(branch_transport, 'lock', lockdir.LockDir)
 
781
        control_files.create_lock()
 
782
        control_files.lock_write()
 
783
        control_files.put_utf8('format', self.get_format_string())
 
784
        try:
 
785
            for file, content in utf8_files:
 
786
                control_files.put_utf8(file, content)
 
787
        finally:
 
788
            control_files.unlock()
 
789
        return self.open(a_bzrdir, _found=True, )
 
790
 
 
791
    def __init__(self):
 
792
        super(BzrBranchFormat5, self).__init__()
 
793
        self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
794
 
 
795
    def open(self, a_bzrdir, _found=False):
 
796
        """Return the branch object for a_bzrdir
 
797
 
 
798
        _found is a private parameter, do not use it. It is used to indicate
 
799
               if format probing has already be done.
 
800
        """
 
801
        if not _found:
 
802
            format = BranchFormat.find_format(a_bzrdir)
 
803
            assert format.__class__ == self.__class__
 
804
        transport = a_bzrdir.get_branch_transport(None)
 
805
        control_files = LockableFiles(transport, 'lock', lockdir.LockDir)
 
806
        return BzrBranch5(_format=self,
 
807
                          _control_files=control_files,
 
808
                          a_bzrdir=a_bzrdir,
 
809
                          _repository=a_bzrdir.find_repository())
 
810
 
 
811
    def __str__(self):
 
812
        return "Bazaar-NG Metadir branch format 5"
 
813
 
 
814
 
 
815
class BranchReferenceFormat(BranchFormat):
 
816
    """Bzr branch reference format.
 
817
 
 
818
    Branch references are used in implementing checkouts, they
 
819
    act as an alias to the real branch which is at some other url.
 
820
 
 
821
    This format has:
 
822
     - A location file
 
823
     - a format string
 
824
    """
 
825
 
 
826
    def get_format_string(self):
 
827
        """See BranchFormat.get_format_string()."""
 
828
        return "Bazaar-NG Branch Reference Format 1\n"
 
829
 
 
830
    def get_format_description(self):
 
831
        """See BranchFormat.get_format_description()."""
 
832
        return "Checkout reference format 1"
 
833
        
 
834
    def initialize(self, a_bzrdir, target_branch=None):
 
835
        """Create a branch of this format in a_bzrdir."""
 
836
        if target_branch is None:
 
837
            # this format does not implement branch itself, thus the implicit
 
838
            # creation contract must see it as uninitializable
 
839
            raise errors.UninitializableFormat(self)
 
840
        mutter('creating branch reference in %s', a_bzrdir.transport.base)
 
841
        branch_transport = a_bzrdir.get_branch_transport(self)
 
842
        # FIXME rbc 20060209 one j-a-ms encoding branch lands this str() cast is not needed.
 
843
        branch_transport.put('location', StringIO(str(target_branch.bzrdir.root_transport.base)))
 
844
        branch_transport.put('format', StringIO(self.get_format_string()))
 
845
        return self.open(a_bzrdir, _found=True)
 
846
 
 
847
    def __init__(self):
 
848
        super(BranchReferenceFormat, self).__init__()
 
849
        self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
850
 
 
851
    def _make_reference_clone_function(format, a_branch):
 
852
        """Create a clone() routine for a branch dynamically."""
 
853
        def clone(to_bzrdir, revision_id=None):
 
854
            """See Branch.clone()."""
 
855
            return format.initialize(to_bzrdir, a_branch)
 
856
            # cannot obey revision_id limits when cloning a reference ...
 
857
            # FIXME RBC 20060210 either nuke revision_id for clone, or
 
858
            # emit some sort of warning/error to the caller ?!
 
859
        return clone
 
860
 
 
861
    def open(self, a_bzrdir, _found=False):
 
862
        """Return the branch that the branch reference in a_bzrdir points at.
 
863
 
 
864
        _found is a private parameter, do not use it. It is used to indicate
 
865
               if format probing has already be done.
 
866
        """
 
867
        if not _found:
 
868
            format = BranchFormat.find_format(a_bzrdir)
 
869
            assert format.__class__ == self.__class__
 
870
        transport = a_bzrdir.get_branch_transport(None)
 
871
        real_bzrdir = bzrdir.BzrDir.open(transport.get('location').read())
 
872
        result = real_bzrdir.open_branch()
 
873
        # this changes the behaviour of result.clone to create a new reference
 
874
        # rather than a copy of the content of the branch.
 
875
        # I did not use a proxy object because that needs much more extensive
 
876
        # testing, and we are only changing one behaviour at the moment.
 
877
        # If we decide to alter more behaviours - i.e. the implicit nickname
 
878
        # then this should be refactored to introduce a tested proxy branch
 
879
        # and a subclass of that for use in overriding clone() and ....
 
880
        # - RBC 20060210
 
881
        result.clone = self._make_reference_clone_function(result)
 
882
        return result
 
883
 
 
884
 
 
885
# formats which have no format string are not discoverable
 
886
# and not independently creatable, so are not registered.
 
887
__default_format = BzrBranchFormat5()
 
888
BranchFormat.register_format(__default_format)
 
889
BranchFormat.register_format(BranchReferenceFormat())
 
890
BranchFormat.set_default_format(__default_format)
 
891
_legacy_formats = [BzrBranchFormat4(),
 
892
                   ]
 
893
 
 
894
class BzrBranch(Branch):
 
895
    """A branch stored in the actual filesystem.
 
896
 
 
897
    Note that it's "local" in the context of the filesystem; it doesn't
 
898
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
899
    it's writable, and can be accessed via the normal filesystem API.
 
900
    """
 
901
    
 
902
    def __init__(self, transport=DEPRECATED_PARAMETER, init=DEPRECATED_PARAMETER,
 
903
                 relax_version_check=DEPRECATED_PARAMETER, _format=None,
 
904
                 _control_files=None, a_bzrdir=None, _repository=None):
 
905
        """Create new branch object at a particular location.
 
906
 
 
907
        transport -- A Transport object, defining how to access files.
 
908
        
 
909
        init -- If True, create new control files in a previously
 
910
             unversioned directory.  If False, the branch must already
 
911
             be versioned.
 
912
 
 
913
        relax_version_check -- If true, the usual check for the branch
 
914
            version is not applied.  This is intended only for
 
915
            upgrade/recovery type use; it's not guaranteed that
 
916
            all operations will work on old format branches.
 
917
        """
 
918
        if a_bzrdir is None:
 
919
            self.bzrdir = bzrdir.BzrDir.open(transport.base)
 
920
        else:
 
921
            self.bzrdir = a_bzrdir
 
922
        self._transport = self.bzrdir.transport.clone('..')
 
923
        self._base = self._transport.base
 
924
        self._format = _format
 
925
        if _control_files is None:
 
926
            raise ValueError('BzrBranch _control_files is None')
 
927
        self.control_files = _control_files
 
928
        if deprecated_passed(init):
 
929
            warn("BzrBranch.__init__(..., init=XXX): The init parameter is "
 
930
                 "deprecated as of bzr 0.8. Please use Branch.create().",
 
931
                 DeprecationWarning,
 
932
                 stacklevel=2)
 
933
            if init:
 
934
                # this is slower than before deprecation, oh well never mind.
 
935
                # -> its deprecated.
 
936
                self._initialize(transport.base)
 
937
        self._check_format(_format)
 
938
        if deprecated_passed(relax_version_check):
 
939
            warn("BzrBranch.__init__(..., relax_version_check=XXX_: The "
 
940
                 "relax_version_check parameter is deprecated as of bzr 0.8. "
 
941
                 "Please use BzrDir.open_downlevel, or a BzrBranchFormat's "
 
942
                 "open() method.",
 
943
                 DeprecationWarning,
 
944
                 stacklevel=2)
 
945
            if (not relax_version_check
 
946
                and not self._format.is_supported()):
 
947
                raise errors.UnsupportedFormatError(format=fmt)
 
948
        if deprecated_passed(transport):
 
949
            warn("BzrBranch.__init__(transport=XXX...): The transport "
 
950
                 "parameter is deprecated as of bzr 0.8. "
 
951
                 "Please use Branch.open, or bzrdir.open_branch().",
 
952
                 DeprecationWarning,
 
953
                 stacklevel=2)
 
954
        self.repository = _repository
 
955
 
 
956
    def __str__(self):
 
957
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
958
 
 
959
    __repr__ = __str__
 
960
 
 
961
    def __del__(self):
 
962
        # TODO: It might be best to do this somewhere else,
 
963
        # but it is nice for a Branch object to automatically
 
964
        # cache it's information.
 
965
        # Alternatively, we could have the Transport objects cache requests
 
966
        # See the earlier discussion about how major objects (like Branch)
 
967
        # should never expect their __del__ function to run.
 
968
        # XXX: cache_root seems to be unused, 2006-01-13 mbp
 
969
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
970
            try:
 
971
                osutils.rmtree(self.cache_root)
 
972
            except:
 
973
                pass
 
974
            self.cache_root = None
 
975
 
 
976
    def _get_base(self):
 
977
        return self._base
 
978
 
 
979
    base = property(_get_base, doc="The URL for the root of this branch.")
 
980
 
 
981
    def _finish_transaction(self):
 
982
        """Exit the current transaction."""
 
983
        return self.control_files._finish_transaction()
 
984
 
 
985
    def get_transaction(self):
 
986
        """Return the current active transaction.
 
987
 
 
988
        If no transaction is active, this returns a passthrough object
 
989
        for which all data is immediately flushed and no caching happens.
 
990
        """
 
991
        # this is an explicit function so that we can do tricky stuff
 
992
        # when the storage in rev_storage is elsewhere.
 
993
        # we probably need to hook the two 'lock a location' and 
 
994
        # 'have a transaction' together more delicately, so that
 
995
        # we can have two locks (branch and storage) and one transaction
 
996
        # ... and finishing the transaction unlocks both, but unlocking
 
997
        # does not. - RBC 20051121
 
998
        return self.control_files.get_transaction()
 
999
 
 
1000
    def _set_transaction(self, transaction):
 
1001
        """Set a new active transaction."""
 
1002
        return self.control_files._set_transaction(transaction)
 
1003
 
 
1004
    def abspath(self, name):
 
1005
        """See Branch.abspath."""
 
1006
        return self.control_files._transport.abspath(name)
 
1007
 
 
1008
    def _check_format(self, format):
 
1009
        """Identify the branch format if needed.
 
1010
 
 
1011
        The format is stored as a reference to the format object in
 
1012
        self._format for code that needs to check it later.
 
1013
 
 
1014
        The format parameter is either None or the branch format class
 
1015
        used to open this branch.
 
1016
 
 
1017
        FIXME: DELETE THIS METHOD when pre 0.8 support is removed.
 
1018
        """
 
1019
        if format is None:
 
1020
            format = BranchFormat.find_format(self.bzrdir)
 
1021
        self._format = format
 
1022
        mutter("got branch format %s", self._format)
 
1023
 
 
1024
    @needs_read_lock
 
1025
    def get_root_id(self):
 
1026
        """See Branch.get_root_id."""
 
1027
        tree = self.repository.revision_tree(self.last_revision())
 
1028
        return tree.inventory.root.file_id
 
1029
 
 
1030
    def is_locked(self):
 
1031
        return self.control_files.is_locked()
 
1032
 
 
1033
    def lock_write(self):
 
1034
        self.repository.lock_write()
 
1035
        try:
 
1036
            self.control_files.lock_write()
 
1037
        except:
 
1038
            self.repository.unlock()
 
1039
            raise
 
1040
 
 
1041
    def lock_read(self):
 
1042
        self.repository.lock_read()
 
1043
        try:
 
1044
            self.control_files.lock_read()
 
1045
        except:
 
1046
            self.repository.unlock()
 
1047
            raise
 
1048
 
 
1049
    def unlock(self):
 
1050
        # TODO: test for failed two phase locks. This is known broken.
 
1051
        try:
 
1052
            self.control_files.unlock()
 
1053
        finally:
 
1054
            self.repository.unlock()
 
1055
        
 
1056
    def peek_lock_mode(self):
 
1057
        if self.control_files._lock_count == 0:
 
1058
            return None
 
1059
        else:
 
1060
            return self.control_files._lock_mode
 
1061
 
 
1062
    def get_physical_lock_status(self):
 
1063
        return self.control_files.get_physical_lock_status()
 
1064
 
 
1065
    @needs_read_lock
 
1066
    def print_file(self, file, revision_id):
 
1067
        """See Branch.print_file."""
 
1068
        return self.repository.print_file(file, revision_id)
 
1069
 
 
1070
    @needs_write_lock
 
1071
    def append_revision(self, *revision_ids):
 
1072
        """See Branch.append_revision."""
 
1073
        for revision_id in revision_ids:
 
1074
            mutter("add {%s} to revision-history" % revision_id)
 
1075
        rev_history = self.revision_history()
 
1076
        rev_history.extend(revision_ids)
 
1077
        self.set_revision_history(rev_history)
 
1078
 
 
1079
    @needs_write_lock
 
1080
    def set_revision_history(self, rev_history):
 
1081
        """See Branch.set_revision_history."""
 
1082
        self.control_files.put_utf8(
 
1083
            'revision-history', '\n'.join(rev_history))
 
1084
        transaction = self.get_transaction()
 
1085
        history = transaction.map.find_revision_history()
 
1086
        if history is not None:
 
1087
            # update the revision history in the identity map.
 
1088
            history[:] = list(rev_history)
 
1089
            # this call is disabled because revision_history is 
 
1090
            # not really an object yet, and the transaction is for objects.
 
1091
            # transaction.register_dirty(history)
 
1092
        else:
 
1093
            transaction.map.add_revision_history(rev_history)
 
1094
            # this call is disabled because revision_history is 
 
1095
            # not really an object yet, and the transaction is for objects.
 
1096
            # transaction.register_clean(history)
 
1097
 
 
1098
    @needs_read_lock
 
1099
    def revision_history(self):
 
1100
        """See Branch.revision_history."""
 
1101
        transaction = self.get_transaction()
 
1102
        history = transaction.map.find_revision_history()
 
1103
        if history is not None:
 
1104
            # mutter("cache hit for revision-history in %s", self)
 
1105
            return list(history)
 
1106
        decode_utf8 = cache_utf8.decode
 
1107
        history = [decode_utf8(l.rstrip('\r\n')) for l in
 
1108
                self.control_files.get('revision-history').readlines()]
 
1109
        transaction.map.add_revision_history(history)
 
1110
        # this call is disabled because revision_history is 
 
1111
        # not really an object yet, and the transaction is for objects.
 
1112
        # transaction.register_clean(history, precious=True)
 
1113
        return list(history)
 
1114
 
 
1115
    @needs_write_lock
 
1116
    def generate_revision_history(self, revision_id, last_rev=None, 
 
1117
        other_branch=None):
 
1118
        """Create a new revision history that will finish with revision_id.
 
1119
        
 
1120
        :param revision_id: the new tip to use.
 
1121
        :param last_rev: The previous last_revision. If not None, then this
 
1122
            must be a ancestory of revision_id, or DivergedBranches is raised.
 
1123
        :param other_branch: The other branch that DivergedBranches should
 
1124
            raise with respect to.
 
1125
        """
 
1126
        # stop_revision must be a descendant of last_revision
 
1127
        stop_graph = self.repository.get_revision_graph(revision_id)
 
1128
        if last_rev is not None and last_rev not in stop_graph:
 
1129
            # our previous tip is not merged into stop_revision
 
1130
            raise errors.DivergedBranches(self, other_branch)
 
1131
        # make a new revision history from the graph
 
1132
        current_rev_id = revision_id
 
1133
        new_history = []
 
1134
        while current_rev_id not in (None, revision.NULL_REVISION):
 
1135
            new_history.append(current_rev_id)
 
1136
            current_rev_id_parents = stop_graph[current_rev_id]
 
1137
            try:
 
1138
                current_rev_id = current_rev_id_parents[0]
 
1139
            except IndexError:
 
1140
                current_rev_id = None
 
1141
        new_history.reverse()
 
1142
        self.set_revision_history(new_history)
 
1143
 
 
1144
    @needs_write_lock
 
1145
    def update_revisions(self, other, stop_revision=None):
 
1146
        """See Branch.update_revisions."""
 
1147
        other.lock_read()
 
1148
        try:
 
1149
            if stop_revision is None:
 
1150
                stop_revision = other.last_revision()
 
1151
                if stop_revision is None:
 
1152
                    # if there are no commits, we're done.
 
1153
                    return
 
1154
            # whats the current last revision, before we fetch [and change it
 
1155
            # possibly]
 
1156
            last_rev = self.last_revision()
 
1157
            # we fetch here regardless of whether we need to so that we pickup
 
1158
            # filled in ghosts.
 
1159
            self.fetch(other, stop_revision)
 
1160
            my_ancestry = self.repository.get_ancestry(last_rev)
 
1161
            if stop_revision in my_ancestry:
 
1162
                # last_revision is a descendant of stop_revision
 
1163
                return
 
1164
            self.generate_revision_history(stop_revision, last_rev=last_rev,
 
1165
                other_branch=other)
 
1166
        finally:
 
1167
            other.unlock()
 
1168
 
 
1169
    def basis_tree(self):
 
1170
        """See Branch.basis_tree."""
 
1171
        return self.repository.revision_tree(self.last_revision())
 
1172
 
 
1173
    @deprecated_method(zero_eight)
592
1174
    def working_tree(self):
593
 
        """Return a `Tree` for the working copy."""
594
 
        return WorkingTree(self.base, self.read_working_inventory())
595
 
 
596
 
 
597
 
    def basis_tree(self):
598
 
        """Return `Tree` object for last revision.
599
 
 
600
 
        If there are no revisions yet, return an `EmptyTree`.
601
 
 
602
 
        >>> b = ScratchBranch(files=['foo'])
603
 
        >>> b.basis_tree().has_filename('foo')
604
 
        False
605
 
        >>> b.working_tree().has_filename('foo')
606
 
        True
607
 
        >>> b.add('foo')
608
 
        >>> b.commit('add foo')
609
 
        >>> b.basis_tree().has_filename('foo')
610
 
        True
611
 
        """
612
 
        r = self.last_patch()
613
 
        if r == None:
614
 
            return EmptyTree()
615
 
        else:
616
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
617
 
 
618
 
 
619
 
 
620
 
    def write_log(self, show_timezone='original'):
621
 
        """Write out human-readable log of commits to this branch
622
 
 
623
 
        :param utc: If true, show dates in universal time, not local time."""
624
 
        ## TODO: Option to choose either original, utc or local timezone
625
 
        revno = 1
626
 
        precursor = None
627
 
        for p in self.revision_history():
628
 
            print '-' * 40
629
 
            print 'revno:', revno
630
 
            ## TODO: Show hash if --id is given.
631
 
            ##print 'revision-hash:', p
632
 
            rev = self.get_revision(p)
633
 
            print 'committer:', rev.committer
634
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
635
 
                                                 show_timezone))
636
 
 
637
 
            ## opportunistic consistency check, same as check_patch_chaining
638
 
            if rev.precursor != precursor:
639
 
                bailout("mismatched precursor!")
640
 
 
641
 
            print 'message:'
642
 
            if not rev.message:
643
 
                print '  (no message)'
644
 
            else:
645
 
                for l in rev.message.split('\n'):
646
 
                    print '  ' + l
647
 
 
648
 
            revno += 1
649
 
            precursor = p
650
 
 
651
 
 
652
 
 
653
 
    def show_status(branch, show_all=False):
654
 
        """Display single-line status for non-ignored working files.
655
 
 
656
 
        The list is show sorted in order by file name.
657
 
 
658
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
659
 
        >>> b.show_status()
660
 
        ?       foo
661
 
        >>> b.add('foo')
662
 
        >>> b.show_status()
663
 
        A       foo
664
 
        >>> b.commit("add foo")
665
 
        >>> b.show_status()
666
 
 
667
 
        :todo: Get state for single files.
668
 
 
669
 
        :todo: Perhaps show a slash at the end of directory names.        
670
 
 
671
 
        """
672
 
 
673
 
        # We have to build everything into a list first so that it can
674
 
        # sorted by name, incorporating all the different sources.
675
 
 
676
 
        # FIXME: Rather than getting things in random order and then sorting,
677
 
        # just step through in order.
678
 
 
679
 
        # Interesting case: the old ID for a file has been removed,
680
 
        # but a new file has been created under that name.
681
 
 
682
 
        old = branch.basis_tree()
683
 
        old_inv = old.inventory
684
 
        new = branch.working_tree()
685
 
        new_inv = new.inventory
686
 
 
687
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
688
 
            if fs == 'R':
689
 
                show_status(fs, kind,
690
 
                            oldname + ' => ' + newname)
691
 
            elif fs == 'A' or fs == 'M':
692
 
                show_status(fs, kind, newname)
693
 
            elif fs == 'D':
694
 
                show_status(fs, kind, oldname)
695
 
            elif fs == '.':
696
 
                if show_all:
697
 
                    show_status(fs, kind, newname)
698
 
            elif fs == 'I':
699
 
                if show_all:
700
 
                    show_status(fs, kind, newname)
701
 
            elif fs == '?':
702
 
                show_status(fs, kind, newname)
703
 
            else:
704
 
                bailout("wierd file state %r" % ((fs, fid),))
705
 
                
706
 
 
707
 
 
708
 
class ScratchBranch(Branch):
709
 
    """Special test class: a branch that cleans up after itself.
710
 
 
711
 
    >>> b = ScratchBranch()
712
 
    >>> isdir(b.base)
713
 
    True
714
 
    >>> bd = b.base
715
 
    >>> del b
716
 
    >>> isdir(bd)
717
 
    False
718
 
    """
719
 
    def __init__(self, files = []):
720
 
        """Make a test branch.
721
 
 
722
 
        This creates a temporary directory and runs init-tree in it.
723
 
 
724
 
        If any files are listed, they are created in the working copy.
725
 
        """
726
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
727
 
        for f in files:
728
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
729
 
 
730
 
 
731
 
    def __del__(self):
732
 
        """Destroy the test branch, removing the scratch directory."""
733
 
        shutil.rmtree(self.base)
734
 
 
 
1175
        """Create a Working tree object for this branch."""
 
1176
 
 
1177
        from bzrlib.transport.local import LocalTransport
 
1178
        if (self.base.find('://') != -1 or 
 
1179
            not isinstance(self._transport, LocalTransport)):
 
1180
            raise NoWorkingTree(self.base)
 
1181
        return self.bzrdir.open_workingtree()
 
1182
 
 
1183
    @needs_write_lock
 
1184
    def pull(self, source, overwrite=False, stop_revision=None):
 
1185
        """See Branch.pull."""
 
1186
        source.lock_read()
 
1187
        try:
 
1188
            old_count = len(self.revision_history())
 
1189
            try:
 
1190
                self.update_revisions(source,stop_revision)
 
1191
            except DivergedBranches:
 
1192
                if not overwrite:
 
1193
                    raise
 
1194
            if overwrite:
 
1195
                self.set_revision_history(source.revision_history())
 
1196
            new_count = len(self.revision_history())
 
1197
            return new_count - old_count
 
1198
        finally:
 
1199
            source.unlock()
 
1200
 
 
1201
    def get_parent(self):
 
1202
        """See Branch.get_parent."""
 
1203
 
 
1204
        _locs = ['parent', 'pull', 'x-pull']
 
1205
        assert self.base[-1] == '/'
 
1206
        for l in _locs:
 
1207
            try:
 
1208
                parent = self.control_files.get(l).read().strip('\n')
 
1209
            except NoSuchFile:
 
1210
                continue
 
1211
            # This is an old-format absolute path to a local branch
 
1212
            # turn it into a url
 
1213
            if parent.startswith('/'):
 
1214
                parent = urlutils.local_path_to_url(parent.decode('utf8'))
 
1215
            try:
 
1216
                return urlutils.join(self.base[:-1], parent)
 
1217
            except errors.InvalidURLJoin, e:
 
1218
                raise errors.InaccessibleParent(parent, self.base)
 
1219
        return None
 
1220
 
 
1221
    def get_push_location(self):
 
1222
        """See Branch.get_push_location."""
 
1223
        push_loc = self.get_config().get_user_option('push_location')
 
1224
        return push_loc
 
1225
 
 
1226
    def set_push_location(self, location):
 
1227
        """See Branch.set_push_location."""
 
1228
        self.get_config().set_user_option('push_location', location, 
 
1229
                                          local=True)
 
1230
 
 
1231
    @needs_write_lock
 
1232
    def set_parent(self, url):
 
1233
        """See Branch.set_parent."""
 
1234
        # TODO: Maybe delete old location files?
 
1235
        # URLs should never be unicode, even on the local fs,
 
1236
        # FIXUP this and get_parent in a future branch format bump:
 
1237
        # read and rewrite the file, and have the new format code read
 
1238
        # using .get not .get_utf8. RBC 20060125
 
1239
        if url is None:
 
1240
            self.control_files._transport.delete('parent')
 
1241
        else:
 
1242
            if isinstance(url, unicode):
 
1243
                try: 
 
1244
                    url = url.encode('ascii')
 
1245
                except UnicodeEncodeError:
 
1246
                    raise bzrlib.errors.InvalidURL(url,
 
1247
                        "Urls must be 7-bit ascii, "
 
1248
                        "use bzrlib.urlutils.escape")
 
1249
                    
 
1250
            url = urlutils.relative_url(self.base, url)
 
1251
            self.control_files.put('parent', url + '\n')
 
1252
 
 
1253
    @deprecated_function(zero_nine)
 
1254
    def tree_config(self):
 
1255
        """DEPRECATED; call get_config instead.  
 
1256
        TreeConfig has become part of BranchConfig."""
 
1257
        return TreeConfig(self)
 
1258
 
 
1259
 
 
1260
class BzrBranch5(BzrBranch):
 
1261
    """A format 5 branch. This supports new features over plan branches.
 
1262
 
 
1263
    It has support for a master_branch which is the data for bound branches.
 
1264
    """
 
1265
 
 
1266
    def __init__(self,
 
1267
                 _format,
 
1268
                 _control_files,
 
1269
                 a_bzrdir,
 
1270
                 _repository):
 
1271
        super(BzrBranch5, self).__init__(_format=_format,
 
1272
                                         _control_files=_control_files,
 
1273
                                         a_bzrdir=a_bzrdir,
 
1274
                                         _repository=_repository)
 
1275
        
 
1276
    @needs_write_lock
 
1277
    def pull(self, source, overwrite=False, stop_revision=None):
 
1278
        """Updates branch.pull to be bound branch aware."""
 
1279
        bound_location = self.get_bound_location()
 
1280
        if source.base != bound_location:
 
1281
            # not pulling from master, so we need to update master.
 
1282
            master_branch = self.get_master_branch()
 
1283
            if master_branch:
 
1284
                master_branch.pull(source)
 
1285
                source = master_branch
 
1286
        return super(BzrBranch5, self).pull(source, overwrite, stop_revision)
 
1287
 
 
1288
    def get_bound_location(self):
 
1289
        try:
 
1290
            return self.control_files.get_utf8('bound').read()[:-1]
 
1291
        except errors.NoSuchFile:
 
1292
            return None
 
1293
 
 
1294
    @needs_read_lock
 
1295
    def get_master_branch(self):
 
1296
        """Return the branch we are bound to.
 
1297
        
 
1298
        :return: Either a Branch, or None
 
1299
 
 
1300
        This could memoise the branch, but if thats done
 
1301
        it must be revalidated on each new lock.
 
1302
        So for now we just don't memoise it.
 
1303
        # RBC 20060304 review this decision.
 
1304
        """
 
1305
        bound_loc = self.get_bound_location()
 
1306
        if not bound_loc:
 
1307
            return None
 
1308
        try:
 
1309
            return Branch.open(bound_loc)
 
1310
        except (errors.NotBranchError, errors.ConnectionError), e:
 
1311
            raise errors.BoundBranchConnectionFailure(
 
1312
                    self, bound_loc, e)
 
1313
 
 
1314
    @needs_write_lock
 
1315
    def set_bound_location(self, location):
 
1316
        """Set the target where this branch is bound to.
 
1317
 
 
1318
        :param location: URL to the target branch
 
1319
        """
 
1320
        if location:
 
1321
            self.control_files.put_utf8('bound', location+'\n')
 
1322
        else:
 
1323
            try:
 
1324
                self.control_files._transport.delete('bound')
 
1325
            except NoSuchFile:
 
1326
                return False
 
1327
            return True
 
1328
 
 
1329
    @needs_write_lock
 
1330
    def bind(self, other):
 
1331
        """Bind the local branch the other branch.
 
1332
 
 
1333
        :param other: The branch to bind to
 
1334
        :type other: Branch
 
1335
        """
 
1336
        # TODO: jam 20051230 Consider checking if the target is bound
 
1337
        #       It is debatable whether you should be able to bind to
 
1338
        #       a branch which is itself bound.
 
1339
        #       Committing is obviously forbidden,
 
1340
        #       but binding itself may not be.
 
1341
        #       Since we *have* to check at commit time, we don't
 
1342
        #       *need* to check here
 
1343
        self.pull(other)
 
1344
 
 
1345
        # we are now equal to or a suffix of other.
 
1346
 
 
1347
        # Since we have 'pulled' from the remote location,
 
1348
        # now we should try to pull in the opposite direction
 
1349
        # in case the local tree has more revisions than the
 
1350
        # remote one.
 
1351
        # There may be a different check you could do here
 
1352
        # rather than actually trying to install revisions remotely.
 
1353
        # TODO: capture an exception which indicates the remote branch
 
1354
        #       is not writable. 
 
1355
        #       If it is up-to-date, this probably should not be a failure
 
1356
        
 
1357
        # lock other for write so the revision-history syncing cannot race
 
1358
        other.lock_write()
 
1359
        try:
 
1360
            other.pull(self)
 
1361
            # if this does not error, other now has the same last rev we do
 
1362
            # it can only error if the pull from other was concurrent with
 
1363
            # a commit to other from someone else.
 
1364
 
 
1365
            # until we ditch revision-history, we need to sync them up:
 
1366
            self.set_revision_history(other.revision_history())
 
1367
            # now other and self are up to date with each other and have the
 
1368
            # same revision-history.
 
1369
        finally:
 
1370
            other.unlock()
 
1371
 
 
1372
        self.set_bound_location(other.base)
 
1373
 
 
1374
    @needs_write_lock
 
1375
    def unbind(self):
 
1376
        """If bound, unbind"""
 
1377
        return self.set_bound_location(None)
 
1378
 
 
1379
    @needs_write_lock
 
1380
    def update(self):
 
1381
        """Synchronise this branch with the master branch if any. 
 
1382
 
 
1383
        :return: None or the last_revision that was pivoted out during the
 
1384
                 update.
 
1385
        """
 
1386
        master = self.get_master_branch()
 
1387
        if master is not None:
 
1388
            old_tip = self.last_revision()
 
1389
            self.pull(master, overwrite=True)
 
1390
            if old_tip in self.repository.get_ancestry(self.last_revision()):
 
1391
                return None
 
1392
            return old_tip
 
1393
        return None
 
1394
 
 
1395
 
 
1396
class BranchTestProviderAdapter(object):
 
1397
    """A tool to generate a suite testing multiple branch formats at once.
 
1398
 
 
1399
    This is done by copying the test once for each transport and injecting
 
1400
    the transport_server, transport_readonly_server, and branch_format
 
1401
    classes into each copy. Each copy is also given a new id() to make it
 
1402
    easy to identify.
 
1403
    """
 
1404
 
 
1405
    def __init__(self, transport_server, transport_readonly_server, formats):
 
1406
        self._transport_server = transport_server
 
1407
        self._transport_readonly_server = transport_readonly_server
 
1408
        self._formats = formats
735
1409
    
 
1410
    def adapt(self, test):
 
1411
        result = TestSuite()
 
1412
        for branch_format, bzrdir_format in self._formats:
 
1413
            new_test = deepcopy(test)
 
1414
            new_test.transport_server = self._transport_server
 
1415
            new_test.transport_readonly_server = self._transport_readonly_server
 
1416
            new_test.bzrdir_format = bzrdir_format
 
1417
            new_test.branch_format = branch_format
 
1418
            def make_new_test_id():
 
1419
                new_id = "%s(%s)" % (new_test.id(), branch_format.__class__.__name__)
 
1420
                return lambda: new_id
 
1421
            new_test.id = make_new_test_id()
 
1422
            result.addTest(new_test)
 
1423
        return result
 
1424
 
 
1425
 
 
1426
class BranchCheckResult(object):
 
1427
    """Results of checking branch consistency.
 
1428
 
 
1429
    :see: Branch.check
 
1430
    """
 
1431
 
 
1432
    def __init__(self, branch):
 
1433
        self.branch = branch
 
1434
 
 
1435
    def report_results(self, verbose):
 
1436
        """Report the check results via trace.note.
 
1437
        
 
1438
        :param verbose: Requests more detailed display of what was checked,
 
1439
            if any.
 
1440
        """
 
1441
        note('checked branch %s format %s',
 
1442
             self.branch.base,
 
1443
             self.branch._format)
 
1444
 
736
1445
 
737
1446
######################################################################
738
1447
# predicates
739
1448
 
740
1449
 
741
 
def is_control_file(filename):
742
 
    ## FIXME: better check
743
 
    filename = os.path.normpath(filename)
744
 
    while filename != '':
745
 
        head, tail = os.path.split(filename)
746
 
        ## mutter('check %r for control file' % ((head, tail), ))
747
 
        if tail == bzrlib.BZRDIR:
748
 
            return True
749
 
        filename = head
750
 
    return False
751
 
 
752
 
 
753
 
 
754
 
def _gen_revision_id(when):
755
 
    """Return new revision-id."""
756
 
    s = '%s-%s-' % (user_email(), compact_date(when))
757
 
    s += hexlify(rand_bytes(8))
758
 
    return s
759
 
 
760
 
 
761
 
def _gen_file_id(name):
762
 
    """Return new file id.
763
 
 
764
 
    This should probably generate proper UUIDs, but for the moment we
765
 
    cope with just randomness because running uuidgen every time is
766
 
    slow."""
767
 
    assert '/' not in name
768
 
    while name[0] == '.':
769
 
        name = name[1:]
770
 
    s = hexlify(rand_bytes(8))
771
 
    return '-'.join((name, compact_date(time.time()), s))
772
 
 
773
 
 
 
1450
@deprecated_function(zero_eight)
 
1451
def is_control_file(*args, **kwargs):
 
1452
    """See bzrlib.workingtree.is_control_file."""
 
1453
    return bzrlib.workingtree.is_control_file(*args, **kwargs)