~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-07 10:31:36 UTC
  • Revision ID: mbp@sourcefrog.net-20050707103135-9b4d911d8df6e880
- fix pwk help

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys
19
 
import os
20
 
import errno
21
 
from warnings import warn
22
 
from cStringIO import StringIO
23
 
 
 
18
import sys, os
24
19
 
25
20
import bzrlib
26
 
from bzrlib.inventory import InventoryEntry
27
 
import bzrlib.inventory as inventory
28
21
from bzrlib.trace import mutter, note
29
 
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
30
 
                            rename, splitpath, sha_file, appendpath, 
31
 
                            file_kind)
32
 
import bzrlib.errors as errors
33
 
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
34
 
                           NoSuchRevision, HistoryMissing, NotBranchError,
35
 
                           DivergedBranches, LockError, UnlistableStore,
36
 
                           UnlistableBranch, NoSuchFile)
37
 
from bzrlib.textui import show_status
38
 
from bzrlib.revision import Revision, is_ancestor, get_intervening_revisions
39
 
 
40
 
from bzrlib.delta import compare_trees
41
 
from bzrlib.tree import EmptyTree, RevisionTree
42
 
from bzrlib.inventory import Inventory
43
 
from bzrlib.store import copy_all
44
 
from bzrlib.store.compressed_text import CompressedTextStore
45
 
from bzrlib.store.text import TextStore
46
 
from bzrlib.store.weave import WeaveStore
47
 
from bzrlib.testament import Testament
48
 
import bzrlib.transactions as transactions
49
 
from bzrlib.transport import Transport, get_transport
50
 
import bzrlib.xml5
51
 
import bzrlib.ui
52
 
 
53
 
 
54
 
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
55
 
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
56
 
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
     sha_file, appendpath, file_kind
 
24
from bzrlib.errors import BzrError
 
25
 
 
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
57
27
## TODO: Maybe include checks for common corruption of newlines, etc?
58
28
 
59
29
 
60
 
# TODO: Some operations like log might retrieve the same revisions
61
 
# repeatedly to calculate deltas.  We could perhaps have a weakref
62
 
# cache in memory to make this faster.  In general anything can be
63
 
# cached in memory between lock and unlock operations.
64
 
 
65
 
def find_branch(*ignored, **ignored_too):
66
 
    # XXX: leave this here for about one release, then remove it
67
 
    raise NotImplementedError('find_branch() is not supported anymore, '
68
 
                              'please use one of the new branch constructors')
 
30
 
 
31
def find_branch(f, **args):
 
32
    if f and (f.startswith('http://') or f.startswith('https://')):
 
33
        import remotebranch 
 
34
        return remotebranch.RemoteBranch(f, **args)
 
35
    else:
 
36
        return Branch(f, **args)
 
37
 
 
38
 
 
39
def find_cached_branch(f, cache_root, **args):
 
40
    from remotebranch import RemoteBranch
 
41
    br = find_branch(f, **args)
 
42
    def cacheify(br, store_name):
 
43
        from meta_store import CachedStore
 
44
        cache_path = os.path.join(cache_root, store_name)
 
45
        os.mkdir(cache_path)
 
46
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
47
        setattr(br, store_name, new_store)
 
48
 
 
49
    if isinstance(br, RemoteBranch):
 
50
        cacheify(br, 'inventory_store')
 
51
        cacheify(br, 'text_store')
 
52
        cacheify(br, 'revision_store')
 
53
    return br
 
54
 
 
55
 
 
56
def _relpath(base, path):
 
57
    """Return path relative to base, or raise exception.
 
58
 
 
59
    The path may be either an absolute path or a path relative to the
 
60
    current working directory.
 
61
 
 
62
    Lifted out of Branch.relpath for ease of testing.
 
63
 
 
64
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
65
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
66
    avoids that problem."""
 
67
    rp = os.path.abspath(path)
 
68
 
 
69
    s = []
 
70
    head = rp
 
71
    while len(head) >= len(base):
 
72
        if head == base:
 
73
            break
 
74
        head, tail = os.path.split(head)
 
75
        if tail:
 
76
            s.insert(0, tail)
 
77
    else:
 
78
        from errors import NotBranchError
 
79
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
80
 
 
81
    return os.sep.join(s)
 
82
        
 
83
 
 
84
def find_branch_root(f=None):
 
85
    """Find the branch root enclosing f, or pwd.
 
86
 
 
87
    f may be a filename or a URL.
 
88
 
 
89
    It is not necessary that f exists.
 
90
 
 
91
    Basically we keep looking up until we find the control directory or
 
92
    run into the root."""
 
93
    if f == None:
 
94
        f = os.getcwd()
 
95
    elif hasattr(os.path, 'realpath'):
 
96
        f = os.path.realpath(f)
 
97
    else:
 
98
        f = os.path.abspath(f)
 
99
    if not os.path.exists(f):
 
100
        raise BzrError('%r does not exist' % f)
 
101
        
 
102
 
 
103
    orig_f = f
 
104
 
 
105
    while True:
 
106
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
107
            return f
 
108
        head, tail = os.path.split(f)
 
109
        if head == f:
 
110
            # reached the root, whatever that may be
 
111
            raise BzrError('%r is not in a branch' % orig_f)
 
112
        f = head
 
113
    
 
114
class DivergedBranches(Exception):
 
115
    def __init__(self, branch1, branch2):
 
116
        self.branch1 = branch1
 
117
        self.branch2 = branch2
 
118
        Exception.__init__(self, "These branches have diverged.")
 
119
 
 
120
 
 
121
class NoSuchRevision(BzrError):
 
122
    def __init__(self, branch, revision):
 
123
        self.branch = branch
 
124
        self.revision = revision
 
125
        msg = "Branch %s has no revision %d" % (branch, revision)
 
126
        BzrError.__init__(self, msg)
 
127
 
69
128
 
70
129
######################################################################
71
130
# branch objects
74
133
    """Branch holding a history of revisions.
75
134
 
76
135
    base
77
 
        Base directory/url of the branch.
78
 
    """
79
 
    base = None
80
 
 
81
 
    def __init__(self, *ignored, **ignored_too):
82
 
        raise NotImplementedError('The Branch class is abstract')
83
 
 
84
 
    @staticmethod
85
 
    def open_downlevel(base):
86
 
        """Open a branch which may be of an old format.
87
 
        
88
 
        Only local branches are supported."""
89
 
        return _Branch(get_transport(base), relax_version_check=True)
90
 
        
91
 
    @staticmethod
92
 
    def open(base):
93
 
        """Open an existing branch, rooted at 'base' (url)"""
94
 
        t = get_transport(base)
95
 
        mutter("trying to open %r with transport %r", base, t)
96
 
        return _Branch(t)
97
 
 
98
 
    @staticmethod
99
 
    def open_containing(url):
100
 
        """Open an existing branch which contains url.
101
 
        
102
 
        This probes for a branch at url, and searches upwards from there.
103
 
 
104
 
        Basically we keep looking up until we find the control directory or
105
 
        run into the root.  If there isn't one, raises NotBranchError.
106
 
        """
107
 
        t = get_transport(url)
108
 
        while True:
109
 
            try:
110
 
                return _Branch(t)
111
 
            except NotBranchError:
112
 
                pass
113
 
            new_t = t.clone('..')
114
 
            if new_t.base == t.base:
115
 
                # reached the root, whatever that may be
116
 
                raise NotBranchError('%s is not in a branch' % url)
117
 
            t = new_t
118
 
 
119
 
    @staticmethod
120
 
    def initialize(base):
121
 
        """Create a new branch, rooted at 'base' (url)"""
122
 
        t = get_transport(base)
123
 
        return _Branch(t, init=True)
124
 
 
125
 
    def setup_caching(self, cache_root):
126
 
        """Subclasses that care about caching should override this, and set
127
 
        up cached stores located under cache_root.
128
 
        """
129
 
        self.cache_root = cache_root
130
 
 
131
 
 
132
 
class _Branch(Branch):
133
 
    """A branch stored in the actual filesystem.
134
 
 
135
 
    Note that it's "local" in the context of the filesystem; it doesn't
136
 
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
137
 
    it's writable, and can be accessed via the normal filesystem API.
 
136
        Base directory of the branch.
138
137
 
139
138
    _lock_mode
140
139
        None, or 'r' or 'w'
146
145
    _lock
147
146
        Lock object from bzrlib.lock.
148
147
    """
149
 
    # We actually expect this class to be somewhat short-lived; part of its
150
 
    # purpose is to try to isolate what bits of the branch logic are tied to
151
 
    # filesystem access, so that in a later step, we can extricate them to
152
 
    # a separarte ("storage") class.
 
148
    base = None
153
149
    _lock_mode = None
154
150
    _lock_count = None
155
151
    _lock = None
156
 
    _inventory_weave = None
157
152
    
158
 
    # Map some sort of prefix into a namespace
159
 
    # stuff like "revno:10", "revid:", etc.
160
 
    # This should match a prefix with a function which accepts
161
 
    REVISION_NAMESPACES = {}
162
 
 
163
 
    def push_stores(self, branch_to):
164
 
        """Copy the content of this branches store to branch_to."""
165
 
        if (self._branch_format != branch_to._branch_format
166
 
            or self._branch_format != 4):
167
 
            from bzrlib.fetch import greedy_fetch
168
 
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
169
 
                   self, self._branch_format, branch_to, branch_to._branch_format)
170
 
            greedy_fetch(to_branch=branch_to, from_branch=self,
171
 
                         revision=self.last_revision())
172
 
            return
173
 
 
174
 
        store_pairs = ((self.text_store,      branch_to.text_store),
175
 
                       (self.inventory_store, branch_to.inventory_store),
176
 
                       (self.revision_store,  branch_to.revision_store))
177
 
        try:
178
 
            for from_store, to_store in store_pairs: 
179
 
                copy_all(from_store, to_store)
180
 
        except UnlistableStore:
181
 
            raise UnlistableBranch(from_store)
182
 
 
183
 
    def __init__(self, transport, init=False,
184
 
                 relax_version_check=False):
 
153
    def __init__(self, base, init=False, find_root=True):
185
154
        """Create new branch object at a particular location.
186
155
 
187
 
        transport -- A Transport object, defining how to access files.
188
 
                (If a string, transport.transport() will be used to
189
 
                create a Transport object)
 
156
        base -- Base directory for the branch.
190
157
        
191
158
        init -- If True, create new control files in a previously
192
159
             unversioned directory.  If False, the branch must already
193
160
             be versioned.
194
161
 
195
 
        relax_version_check -- If true, the usual check for the branch
196
 
            version is not applied.  This is intended only for
197
 
            upgrade/recovery type use; it's not guaranteed that
198
 
            all operations will work on old format branches.
 
162
        find_root -- If true and init is false, find the root of the
 
163
             existing branch containing base.
199
164
 
200
165
        In the test suite, creation of new trees is tested using the
201
166
        `ScratchBranch` class.
202
167
        """
203
 
        assert isinstance(transport, Transport), \
204
 
            "%r is not a Transport" % transport
205
 
        self._transport = transport
 
168
        from bzrlib.store import ImmutableStore
206
169
        if init:
 
170
            self.base = os.path.realpath(base)
207
171
            self._make_control()
208
 
        self._check_format(relax_version_check)
209
 
 
210
 
        def get_store(name, compressed=True, prefixed=False):
211
 
            # FIXME: This approach of assuming stores are all entirely compressed
212
 
            # or entirely uncompressed is tidy, but breaks upgrade from 
213
 
            # some existing branches where there's a mixture; we probably 
214
 
            # still want the option to look for both.
215
 
            relpath = self._rel_controlfilename(name)
216
 
            if compressed:
217
 
                store = CompressedTextStore(self._transport.clone(relpath),
218
 
                                            prefixed=prefixed)
219
 
            else:
220
 
                store = TextStore(self._transport.clone(relpath),
221
 
                                  prefixed=prefixed)
222
 
            #if self._transport.should_cache():
223
 
            #    cache_path = os.path.join(self.cache_root, name)
224
 
            #    os.mkdir(cache_path)
225
 
            #    store = bzrlib.store.CachedStore(store, cache_path)
226
 
            return store
227
 
        def get_weave(name, prefixed=False):
228
 
            relpath = self._rel_controlfilename(name)
229
 
            ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
230
 
            if self._transport.should_cache():
231
 
                ws.enable_cache = True
232
 
            return ws
233
 
 
234
 
        if self._branch_format == 4:
235
 
            self.inventory_store = get_store('inventory-store')
236
 
            self.text_store = get_store('text-store')
237
 
            self.revision_store = get_store('revision-store')
238
 
        elif self._branch_format == 5:
239
 
            self.control_weaves = get_weave([])
240
 
            self.weave_store = get_weave('weaves')
241
 
            self.revision_store = get_store('revision-store', compressed=False)
242
 
        elif self._branch_format == 6:
243
 
            self.control_weaves = get_weave([])
244
 
            self.weave_store = get_weave('weaves', prefixed=True)
245
 
            self.revision_store = get_store('revision-store', compressed=False,
246
 
                                            prefixed=True)
247
 
        self.revision_store.register_suffix('sig')
248
 
        self._transaction = None
 
172
        elif find_root:
 
173
            self.base = find_branch_root(base)
 
174
        else:
 
175
            self.base = os.path.realpath(base)
 
176
            if not isdir(self.controlfilename('.')):
 
177
                from errors import NotBranchError
 
178
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
179
                                     ['use "bzr init" to initialize a new working tree',
 
180
                                      'current bzr can only operate from top-of-tree'])
 
181
        self._check_format()
 
182
 
 
183
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
184
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
185
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
186
 
249
187
 
250
188
    def __str__(self):
251
 
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
 
189
        return '%s(%r)' % (self.__class__.__name__, self.base)
252
190
 
253
191
 
254
192
    __repr__ = __str__
256
194
 
257
195
    def __del__(self):
258
196
        if self._lock_mode or self._lock:
259
 
            # XXX: This should show something every time, and be suitable for
260
 
            # headless operation and embedding
 
197
            from warnings import warn
261
198
            warn("branch %r was not explicitly unlocked" % self)
262
199
            self._lock.unlock()
263
200
 
264
 
        # TODO: It might be best to do this somewhere else,
265
 
        # but it is nice for a Branch object to automatically
266
 
        # cache it's information.
267
 
        # Alternatively, we could have the Transport objects cache requests
268
 
        # See the earlier discussion about how major objects (like Branch)
269
 
        # should never expect their __del__ function to run.
270
 
        if hasattr(self, 'cache_root') and self.cache_root is not None:
271
 
            try:
272
 
                import shutil
273
 
                shutil.rmtree(self.cache_root)
274
 
            except:
275
 
                pass
276
 
            self.cache_root = None
277
 
 
278
 
    def _get_base(self):
279
 
        if self._transport:
280
 
            return self._transport.base
281
 
        return None
282
 
 
283
 
    base = property(_get_base, doc="The URL for the root of this branch.")
284
 
 
285
 
    def _finish_transaction(self):
286
 
        """Exit the current transaction."""
287
 
        if self._transaction is None:
288
 
            raise errors.LockError('Branch %s is not in a transaction' %
289
 
                                   self)
290
 
        transaction = self._transaction
291
 
        self._transaction = None
292
 
        transaction.finish()
293
 
 
294
 
    def get_transaction(self):
295
 
        """Return the current active transaction.
296
 
 
297
 
        If no transaction is active, this returns a passthrough object
298
 
        for which all data is immedaitely flushed and no caching happens.
299
 
        """
300
 
        if self._transaction is None:
301
 
            return transactions.PassThroughTransaction()
302
 
        else:
303
 
            return self._transaction
304
 
 
305
 
    def _set_transaction(self, new_transaction):
306
 
        """Set a new active transaction."""
307
 
        if self._transaction is not None:
308
 
            raise errors.LockError('Branch %s is in a transaction already.' %
309
 
                                   self)
310
 
        self._transaction = new_transaction
 
201
 
311
202
 
312
203
    def lock_write(self):
313
 
        mutter("lock write: %s (%s)", self, self._lock_count)
314
 
        # TODO: Upgrade locking to support using a Transport,
315
 
        # and potentially a remote locking protocol
316
204
        if self._lock_mode:
317
205
            if self._lock_mode != 'w':
 
206
                from errors import LockError
318
207
                raise LockError("can't upgrade to a write lock from %r" %
319
208
                                self._lock_mode)
320
209
            self._lock_count += 1
321
210
        else:
322
 
            self._lock = self._transport.lock_write(
323
 
                    self._rel_controlfilename('branch-lock'))
 
211
            from bzrlib.lock import WriteLock
 
212
 
 
213
            self._lock = WriteLock(self.controlfilename('branch-lock'))
324
214
            self._lock_mode = 'w'
325
215
            self._lock_count = 1
326
 
            self._set_transaction(transactions.PassThroughTransaction())
 
216
 
 
217
 
327
218
 
328
219
    def lock_read(self):
329
 
        mutter("lock read: %s (%s)", self, self._lock_count)
330
220
        if self._lock_mode:
331
221
            assert self._lock_mode in ('r', 'w'), \
332
222
                   "invalid lock mode %r" % self._lock_mode
333
223
            self._lock_count += 1
334
224
        else:
335
 
            self._lock = self._transport.lock_read(
336
 
                    self._rel_controlfilename('branch-lock'))
 
225
            from bzrlib.lock import ReadLock
 
226
 
 
227
            self._lock = ReadLock(self.controlfilename('branch-lock'))
337
228
            self._lock_mode = 'r'
338
229
            self._lock_count = 1
339
 
            self._set_transaction(transactions.ReadOnlyTransaction())
340
 
            # 5K may be excessive, but hey, its a knob.
341
 
            self.get_transaction().set_cache_size(5000)
342
230
                        
 
231
 
 
232
            
343
233
    def unlock(self):
344
 
        mutter("unlock: %s (%s)", self, self._lock_count)
345
234
        if not self._lock_mode:
 
235
            from errors import LockError
346
236
            raise LockError('branch %r is not locked' % (self))
347
237
 
348
238
        if self._lock_count > 1:
349
239
            self._lock_count -= 1
350
240
        else:
351
 
            self._finish_transaction()
352
241
            self._lock.unlock()
353
242
            self._lock = None
354
243
            self._lock_mode = self._lock_count = None
355
244
 
 
245
 
356
246
    def abspath(self, name):
357
 
        """Return absolute filename for something in the branch
358
 
        
359
 
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
360
 
        method and not a tree method.
361
 
        """
362
 
        return self._transport.abspath(name)
363
 
 
364
 
    def _rel_controlfilename(self, file_or_path):
 
247
        """Return absolute filename for something in the branch"""
 
248
        return os.path.join(self.base, name)
 
249
 
 
250
 
 
251
    def relpath(self, path):
 
252
        """Return path relative to this branch of something inside it.
 
253
 
 
254
        Raises an error if path is not in this branch."""
 
255
        return _relpath(self.base, path)
 
256
 
 
257
 
 
258
    def controlfilename(self, file_or_path):
 
259
        """Return location relative to branch."""
365
260
        if isinstance(file_or_path, basestring):
366
261
            file_or_path = [file_or_path]
367
 
        return [bzrlib.BZRDIR] + file_or_path
368
 
 
369
 
    def controlfilename(self, file_or_path):
370
 
        """Return location relative to branch."""
371
 
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
 
262
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
372
263
 
373
264
 
374
265
    def controlfile(self, file_or_path, mode='r'):
382
273
        Controlfiles should almost never be opened in write mode but
383
274
        rather should be atomically copied and replaced using atomicfile.
384
275
        """
385
 
        import codecs
386
 
 
387
 
        relpath = self._rel_controlfilename(file_or_path)
388
 
        #TODO: codecs.open() buffers linewise, so it was overloaded with
389
 
        # a much larger buffer, do we need to do the same for getreader/getwriter?
390
 
        if mode == 'rb': 
391
 
            return self._transport.get(relpath)
392
 
        elif mode == 'wb':
393
 
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
394
 
        elif mode == 'r':
395
 
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
396
 
        elif mode == 'w':
397
 
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
 
276
 
 
277
        fn = self.controlfilename(file_or_path)
 
278
 
 
279
        if mode == 'rb' or mode == 'wb':
 
280
            return file(fn, mode)
 
281
        elif mode == 'r' or mode == 'w':
 
282
            # open in binary mode anyhow so there's no newline translation;
 
283
            # codecs uses line buffering by default; don't want that.
 
284
            import codecs
 
285
            return codecs.open(fn, mode + 'b', 'utf-8',
 
286
                               buffering=60000)
398
287
        else:
399
288
            raise BzrError("invalid controlfile mode %r" % mode)
400
289
 
401
 
    def put_controlfile(self, path, f, encode=True):
402
 
        """Write an entry as a controlfile.
403
 
 
404
 
        :param path: The path to put the file, relative to the .bzr control
405
 
                     directory
406
 
        :param f: A file-like or string object whose contents should be copied.
407
 
        :param encode:  If true, encode the contents as utf-8
408
 
        """
409
 
        self.put_controlfiles([(path, f)], encode=encode)
410
 
 
411
 
    def put_controlfiles(self, files, encode=True):
412
 
        """Write several entries as controlfiles.
413
 
 
414
 
        :param files: A list of [(path, file)] pairs, where the path is the directory
415
 
                      underneath the bzr control directory
416
 
        :param encode:  If true, encode the contents as utf-8
417
 
        """
418
 
        import codecs
419
 
        ctrl_files = []
420
 
        for path, f in files:
421
 
            if encode:
422
 
                if isinstance(f, basestring):
423
 
                    f = f.encode('utf-8', 'replace')
424
 
                else:
425
 
                    f = codecs.getwriter('utf-8')(f, errors='replace')
426
 
            path = self._rel_controlfilename(path)
427
 
            ctrl_files.append((path, f))
428
 
        self._transport.put_multi(ctrl_files)
 
290
 
429
291
 
430
292
    def _make_control(self):
431
293
        from bzrlib.inventory import Inventory
432
 
        from bzrlib.weavefile import write_weave_v5
433
 
        from bzrlib.weave import Weave
 
294
        from bzrlib.xml import pack_xml
434
295
        
435
 
        # Create an empty inventory
436
 
        sio = StringIO()
437
 
        # if we want per-tree root ids then this is the place to set
438
 
        # them; they're not needed for now and so ommitted for
439
 
        # simplicity.
440
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
441
 
        empty_inv = sio.getvalue()
442
 
        sio = StringIO()
443
 
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
444
 
        empty_weave = sio.getvalue()
445
 
 
446
 
        dirs = [[], 'revision-store', 'weaves']
447
 
        files = [('README', 
 
296
        os.mkdir(self.controlfilename([]))
 
297
        self.controlfile('README', 'w').write(
448
298
            "This is a Bazaar-NG control directory.\n"
449
 
            "Do not change any files in this directory.\n"),
450
 
            ('branch-format', BZR_BRANCH_FORMAT_6),
451
 
            ('revision-history', ''),
452
 
            ('branch-name', ''),
453
 
            ('branch-lock', ''),
454
 
            ('pending-merges', ''),
455
 
            ('inventory', empty_inv),
456
 
            ('inventory.weave', empty_weave),
457
 
            ('ancestry.weave', empty_weave)
458
 
        ]
459
 
        cfn = self._rel_controlfilename
460
 
        self._transport.mkdir_multi([cfn(d) for d in dirs])
461
 
        self.put_controlfiles(files)
462
 
        mutter('created control directory in ' + self._transport.base)
463
 
 
464
 
    def _check_format(self, relax_version_check):
 
299
            "Do not change any files in this directory.\n")
 
300
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
301
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
302
            os.mkdir(self.controlfilename(d))
 
303
        for f in ('revision-history', 'merged-patches',
 
304
                  'pending-merged-patches', 'branch-name',
 
305
                  'branch-lock',
 
306
                  'pending-merges'):
 
307
            self.controlfile(f, 'w').write('')
 
308
        mutter('created control directory in ' + self.base)
 
309
 
 
310
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
311
 
 
312
 
 
313
    def _check_format(self):
465
314
        """Check this branch format is supported.
466
315
 
467
 
        The format level is stored, as an integer, in
468
 
        self._branch_format for code that needs to check it later.
 
316
        The current tool only supports the current unstable format.
469
317
 
470
318
        In the future, we might need different in-memory Branch
471
319
        classes to support downlevel branches.  But not yet.
472
320
        """
473
 
        try:
474
 
            fmt = self.controlfile('branch-format', 'r').read()
475
 
        except NoSuchFile:
476
 
            raise NotBranchError(self.base)
477
 
        mutter("got branch format %r", fmt)
478
 
        if fmt == BZR_BRANCH_FORMAT_6:
479
 
            self._branch_format = 6
480
 
        elif fmt == BZR_BRANCH_FORMAT_5:
481
 
            self._branch_format = 5
482
 
        elif fmt == BZR_BRANCH_FORMAT_4:
483
 
            self._branch_format = 4
484
 
 
485
 
        if (not relax_version_check
486
 
            and self._branch_format not in (5, 6)):
487
 
            raise errors.UnsupportedFormatError(
488
 
                           'sorry, branch format %r not supported' % fmt,
 
321
        # This ignores newlines so that we can open branches created
 
322
        # on Windows from Linux and so on.  I think it might be better
 
323
        # to always make all internal files in unix format.
 
324
        fmt = self.controlfile('branch-format', 'r').read()
 
325
        fmt.replace('\r\n', '')
 
326
        if fmt != BZR_BRANCH_FORMAT:
 
327
            raise BzrError('sorry, branch format %r not supported' % fmt,
489
328
                           ['use a different bzr version',
490
 
                            'or remove the .bzr directory'
491
 
                            ' and "bzr init" again'])
492
 
 
493
 
    def get_root_id(self):
494
 
        """Return the id of this branches root"""
495
 
        inv = self.read_working_inventory()
496
 
        return inv.root.file_id
497
 
 
498
 
    def set_root_id(self, file_id):
499
 
        inv = self.read_working_inventory()
500
 
        orig_root_id = inv.root.file_id
501
 
        del inv._byid[inv.root.file_id]
502
 
        inv.root.file_id = file_id
503
 
        inv._byid[inv.root.file_id] = inv.root
504
 
        for fid in inv:
505
 
            entry = inv[fid]
506
 
            if entry.parent_id in (None, orig_root_id):
507
 
                entry.parent_id = inv.root.file_id
508
 
        self._write_inventory(inv)
 
329
                            'or remove the .bzr directory and "bzr init" again'])
 
330
 
 
331
 
509
332
 
510
333
    def read_working_inventory(self):
511
334
        """Read the working inventory."""
 
335
        from bzrlib.inventory import Inventory
 
336
        from bzrlib.xml import unpack_xml
 
337
        from time import time
 
338
        before = time()
512
339
        self.lock_read()
513
340
        try:
514
341
            # ElementTree does its own conversion from UTF-8, so open in
515
342
            # binary.
516
 
            f = self.controlfile('inventory', 'rb')
517
 
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
343
            inv = unpack_xml(Inventory,
 
344
                                  self.controlfile('inventory', 'rb'))
 
345
            mutter("loaded inventory of %d items in %f"
 
346
                   % (len(inv), time() - before))
 
347
            return inv
518
348
        finally:
519
349
            self.unlock()
520
350
            
525
355
        That is to say, the inventory describing changes underway, that
526
356
        will be committed to the next revision.
527
357
        """
528
 
        from cStringIO import StringIO
 
358
        from bzrlib.atomicfile import AtomicFile
 
359
        from bzrlib.xml import pack_xml
 
360
        
529
361
        self.lock_write()
530
362
        try:
531
 
            sio = StringIO()
532
 
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
533
 
            sio.seek(0)
534
 
            # Transport handles atomicity
535
 
            self.put_controlfile('inventory', sio)
 
363
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
364
            try:
 
365
                pack_xml(inv, f)
 
366
                f.commit()
 
367
            finally:
 
368
                f.close()
536
369
        finally:
537
370
            self.unlock()
538
371
        
539
372
        mutter('wrote working inventory')
540
373
            
 
374
 
541
375
    inventory = property(read_working_inventory, _write_inventory, None,
542
376
                         """Inventory for the working copy.""")
543
377
 
544
 
    def add(self, files, ids=None):
 
378
 
 
379
    def add(self, files, verbose=False, ids=None):
545
380
        """Make files versioned.
546
381
 
547
 
        Note that the command line normally calls smart_add instead,
548
 
        which can automatically recurse.
 
382
        Note that the command line normally calls smart_add instead.
549
383
 
550
384
        This puts the files in the Added state, so that they will be
551
385
        recorded by the next commit.
561
395
        TODO: Perhaps have an option to add the ids even if the files do
562
396
              not (yet) exist.
563
397
 
564
 
        TODO: Perhaps yield the ids and paths as they're added.
 
398
        TODO: Perhaps return the ids of the files?  But then again it
 
399
              is easy to retrieve them if they're needed.
 
400
 
 
401
        TODO: Adding a directory should optionally recurse down and
 
402
              add all non-ignored children.  Perhaps do that in a
 
403
              higher-level method.
565
404
        """
 
405
        from bzrlib.textui import show_status
566
406
        # TODO: Re-adding a file that is removed in the working copy
567
407
        # should probably put it back with the previous ID.
568
408
        if isinstance(files, basestring):
594
434
                    kind = file_kind(fullpath)
595
435
                except OSError:
596
436
                    # maybe something better?
597
 
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
437
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
598
438
 
599
 
                if not InventoryEntry.versionable_kind(kind):
600
 
                    raise BzrError('cannot add: not a versionable file ('
601
 
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
439
                if kind != 'file' and kind != 'directory':
 
440
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
602
441
 
603
442
                if file_id is None:
604
443
                    file_id = gen_file_id(f)
605
444
                inv.add_path(f, kind=kind, file_id=file_id)
606
445
 
 
446
                if verbose:
 
447
                    print 'added', quotefn(f)
 
448
 
607
449
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
608
450
 
609
451
            self._write_inventory(inv)
615
457
        """Print `file` to stdout."""
616
458
        self.lock_read()
617
459
        try:
618
 
            tree = self.revision_tree(self.get_rev_id(revno))
 
460
            tree = self.revision_tree(self.lookup_revision(revno))
619
461
            # use inventory as it was in that revision
620
462
            file_id = tree.inventory.path2id(file)
621
463
            if not file_id:
622
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
464
                raise BzrError("%r is not present in revision %d" % (file, revno))
623
465
            tree.print_file(file_id)
624
466
        finally:
625
467
            self.unlock()
639
481
        is the opposite of add.  Removing it is consistent with most
640
482
        other tools.  Maybe an option.
641
483
        """
 
484
        from bzrlib.textui import show_status
642
485
        ## TODO: Normalize names
643
486
        ## TODO: Remove nested loops; better scalability
644
487
        if isinstance(files, basestring):
669
512
        finally:
670
513
            self.unlock()
671
514
 
 
515
 
672
516
    # FIXME: this doesn't need to be a branch method
673
517
    def set_inventory(self, new_inventory_list):
674
518
        from bzrlib.inventory import Inventory, InventoryEntry
675
 
        inv = Inventory(self.get_root_id())
 
519
        inv = Inventory()
676
520
        for path, file_id, parent, kind in new_inventory_list:
677
521
            name = os.path.basename(path)
678
522
            if name == "":
679
523
                continue
680
 
            # fixme, there should be a factory function inv,add_?? 
681
 
            if kind == 'directory':
682
 
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
683
 
            elif kind == 'file':
684
 
                inv.add(inventory.InventoryFile(file_id, name, parent))
685
 
            elif kind == 'symlink':
686
 
                inv.add(inventory.InventoryLink(file_id, name, parent))
687
 
            else:
688
 
                raise BzrError("unknown kind %r" % kind)
 
524
            inv.add(InventoryEntry(file_id, name, kind, parent))
689
525
        self._write_inventory(inv)
690
526
 
 
527
 
691
528
    def unknowns(self):
692
529
        """Return all unknown files.
693
530
 
707
544
        return self.working_tree().unknowns()
708
545
 
709
546
 
710
 
    def append_revision(self, *revision_ids):
711
 
        for revision_id in revision_ids:
712
 
            mutter("add {%s} to revision-history" % revision_id)
713
 
        self.lock_write()
714
 
        try:
715
 
            rev_history = self.revision_history()
716
 
            rev_history.extend(revision_ids)
717
 
            self.put_controlfile('revision-history', '\n'.join(rev_history))
718
 
        finally:
719
 
            self.unlock()
720
 
 
721
 
    def has_revision(self, revision_id):
722
 
        """True if this branch has a copy of the revision.
723
 
 
724
 
        This does not necessarily imply the revision is merge
725
 
        or on the mainline."""
726
 
        return (revision_id is None
727
 
                or self.revision_store.has_id(revision_id))
728
 
 
729
 
    def get_revision_xml_file(self, revision_id):
730
 
        """Return XML file object for revision object."""
731
 
        if not revision_id or not isinstance(revision_id, basestring):
732
 
            raise InvalidRevisionId(revision_id)
733
 
 
734
 
        self.lock_read()
735
 
        try:
736
 
            try:
737
 
                return self.revision_store.get(revision_id)
738
 
            except (IndexError, KeyError):
739
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
740
 
        finally:
741
 
            self.unlock()
742
 
 
743
 
    #deprecated
744
 
    get_revision_xml = get_revision_xml_file
745
 
 
746
 
    def get_revision_xml(self, revision_id):
747
 
        return self.get_revision_xml_file(revision_id).read()
 
547
    def append_revision(self, revision_id):
 
548
        from bzrlib.atomicfile import AtomicFile
 
549
 
 
550
        mutter("add {%s} to revision-history" % revision_id)
 
551
        rev_history = self.revision_history() + [revision_id]
 
552
 
 
553
        f = AtomicFile(self.controlfilename('revision-history'))
 
554
        try:
 
555
            for rev_id in rev_history:
 
556
                print >>f, rev_id
 
557
            f.commit()
 
558
        finally:
 
559
            f.close()
748
560
 
749
561
 
750
562
    def get_revision(self, revision_id):
751
563
        """Return the Revision object for a named revision"""
752
 
        xml_file = self.get_revision_xml_file(revision_id)
 
564
        from bzrlib.revision import Revision
 
565
        from bzrlib.xml import unpack_xml
753
566
 
 
567
        self.lock_read()
754
568
        try:
755
 
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
756
 
        except SyntaxError, e:
757
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
758
 
                                         [revision_id,
759
 
                                          str(e)])
 
569
            if not revision_id or not isinstance(revision_id, basestring):
 
570
                raise ValueError('invalid revision-id: %r' % revision_id)
 
571
            r = unpack_xml(Revision, self.revision_store[revision_id])
 
572
        finally:
 
573
            self.unlock()
760
574
            
761
575
        assert r.revision_id == revision_id
762
576
        return r
763
 
 
764
 
    def get_revision_delta(self, revno):
765
 
        """Return the delta for one revision.
766
 
 
767
 
        The delta is relative to its mainline predecessor, or the
768
 
        empty tree for revision 1.
769
 
        """
770
 
        assert isinstance(revno, int)
771
 
        rh = self.revision_history()
772
 
        if not (1 <= revno <= len(rh)):
773
 
            raise InvalidRevisionNumber(revno)
774
 
 
775
 
        # revno is 1-based; list is 0-based
776
 
 
777
 
        new_tree = self.revision_tree(rh[revno-1])
778
 
        if revno == 1:
779
 
            old_tree = EmptyTree()
780
 
        else:
781
 
            old_tree = self.revision_tree(rh[revno-2])
782
 
 
783
 
        return compare_trees(old_tree, new_tree)
 
577
        
784
578
 
785
579
    def get_revision_sha1(self, revision_id):
786
580
        """Hash the stored value of a revision, and return it."""
790
584
        # the revision, (add signatures/remove signatures) and still
791
585
        # have all hash pointers stay consistent.
792
586
        # But for now, just hash the contents.
793
 
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
794
 
 
795
 
    def get_ancestry(self, revision_id):
796
 
        """Return a list of revision-ids integrated by a revision.
797
 
        
798
 
        This currently returns a list, but the ordering is not guaranteed:
799
 
        treat it as a set.
800
 
        """
801
 
        if revision_id is None:
802
 
            return [None]
803
 
        w = self.get_inventory_weave()
804
 
        return [None] + map(w.idx_to_name,
805
 
                            w.inclusions([w.lookup(revision_id)]))
806
 
 
807
 
    def get_inventory_weave(self):
808
 
        return self.control_weaves.get_weave('inventory',
809
 
                                             self.get_transaction())
810
 
 
811
 
    def get_inventory(self, revision_id):
812
 
        """Get Inventory object by hash."""
813
 
        xml = self.get_inventory_xml(revision_id)
814
 
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
815
 
 
816
 
    def get_inventory_xml(self, revision_id):
817
 
        """Get inventory XML as a file object."""
818
 
        try:
819
 
            assert isinstance(revision_id, basestring), type(revision_id)
820
 
            iw = self.get_inventory_weave()
821
 
            return iw.get_text(iw.lookup(revision_id))
822
 
        except IndexError:
823
 
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
824
 
 
825
 
    def get_inventory_sha1(self, revision_id):
 
587
        return sha_file(self.revision_store[revision_id])
 
588
 
 
589
 
 
590
    def get_inventory(self, inventory_id):
 
591
        """Get Inventory object by hash.
 
592
 
 
593
        TODO: Perhaps for this and similar methods, take a revision
 
594
               parameter which can be either an integer revno or a
 
595
               string hash."""
 
596
        from bzrlib.inventory import Inventory
 
597
        from bzrlib.xml import unpack_xml
 
598
 
 
599
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
600
            
 
601
 
 
602
    def get_inventory_sha1(self, inventory_id):
826
603
        """Return the sha1 hash of the inventory entry
827
604
        """
828
 
        return self.get_revision(revision_id).inventory_sha1
 
605
        return sha_file(self.inventory_store[inventory_id])
 
606
 
829
607
 
830
608
    def get_revision_inventory(self, revision_id):
831
609
        """Return inventory of a past revision."""
832
 
        # TODO: Unify this with get_inventory()
833
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
610
        # bzr 0.0.6 imposes the constraint that the inventory_id
834
611
        # must be the same as its revision, so this is trivial.
835
612
        if revision_id == None:
836
 
            return Inventory(self.get_root_id())
 
613
            from bzrlib.inventory import Inventory
 
614
            return Inventory()
837
615
        else:
838
616
            return self.get_inventory(revision_id)
839
617
 
 
618
 
840
619
    def revision_history(self):
841
 
        """Return sequence of revision hashes on to this branch."""
 
620
        """Return sequence of revision hashes on to this branch.
 
621
 
 
622
        >>> ScratchBranch().revision_history()
 
623
        []
 
624
        """
842
625
        self.lock_read()
843
626
        try:
844
 
            transaction = self.get_transaction()
845
 
            history = transaction.map.find_revision_history()
846
 
            if history is not None:
847
 
                mutter("cache hit for revision-history in %s", self)
848
 
                return list(history)
849
 
            history = [l.rstrip('\r\n') for l in
 
627
            return [l.rstrip('\r\n') for l in
850
628
                    self.controlfile('revision-history', 'r').readlines()]
851
 
            transaction.map.add_revision_history(history)
852
 
            # this call is disabled because revision_history is 
853
 
            # not really an object yet, and the transaction is for objects.
854
 
            # transaction.register_clean(history, precious=True)
855
 
            return list(history)
856
629
        finally:
857
630
            self.unlock()
858
631
 
 
632
 
 
633
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
634
        """
 
635
        >>> import commit
 
636
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
637
        >>> sb.common_ancestor(sb) == (None, None)
 
638
        True
 
639
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
640
        >>> sb.common_ancestor(sb)[0]
 
641
        1
 
642
        >>> clone = sb.clone()
 
643
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
644
        >>> sb.common_ancestor(sb)[0]
 
645
        2
 
646
        >>> sb.common_ancestor(clone)[0]
 
647
        1
 
648
        >>> commit.commit(clone, "Committing divergent second revision", 
 
649
        ...               verbose=False)
 
650
        >>> sb.common_ancestor(clone)[0]
 
651
        1
 
652
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
653
        True
 
654
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
655
        True
 
656
        >>> clone2 = sb.clone()
 
657
        >>> sb.common_ancestor(clone2)[0]
 
658
        2
 
659
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
660
        1
 
661
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
662
        1
 
663
        """
 
664
        my_history = self.revision_history()
 
665
        other_history = other.revision_history()
 
666
        if self_revno is None:
 
667
            self_revno = len(my_history)
 
668
        if other_revno is None:
 
669
            other_revno = len(other_history)
 
670
        indices = range(min((self_revno, other_revno)))
 
671
        indices.reverse()
 
672
        for r in indices:
 
673
            if my_history[r] == other_history[r]:
 
674
                return r+1, my_history[r]
 
675
        return None, None
 
676
 
 
677
    def enum_history(self, direction):
 
678
        """Return (revno, revision_id) for history of branch.
 
679
 
 
680
        direction
 
681
            'forward' is from earliest to latest
 
682
            'reverse' is from latest to earliest
 
683
        """
 
684
        rh = self.revision_history()
 
685
        if direction == 'forward':
 
686
            i = 1
 
687
            for rid in rh:
 
688
                yield i, rid
 
689
                i += 1
 
690
        elif direction == 'reverse':
 
691
            i = len(rh)
 
692
            while i > 0:
 
693
                yield i, rh[i-1]
 
694
                i -= 1
 
695
        else:
 
696
            raise ValueError('invalid history direction', direction)
 
697
 
 
698
 
859
699
    def revno(self):
860
700
        """Return current revision number for this branch.
861
701
 
865
705
        return len(self.revision_history())
866
706
 
867
707
 
868
 
    def last_revision(self):
 
708
    def last_patch(self):
869
709
        """Return last patch hash, or None if no history.
870
710
        """
871
711
        ph = self.revision_history()
875
715
            return None
876
716
 
877
717
 
878
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
879
 
        """Return a list of new revisions that would perfectly fit.
880
 
        
 
718
    def missing_revisions(self, other, stop_revision=None):
 
719
        """
881
720
        If self and other have not diverged, return a list of the revisions
882
721
        present in other, but missing from self.
883
722
 
914
753
 
915
754
        if stop_revision is None:
916
755
            stop_revision = other_len
917
 
        else:
918
 
            assert isinstance(stop_revision, int)
919
 
            if stop_revision > other_len:
920
 
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
756
        elif stop_revision > other_len:
 
757
            raise NoSuchRevision(self, stop_revision)
 
758
        
921
759
        return other_history[self_len:stop_revision]
922
760
 
 
761
 
923
762
    def update_revisions(self, other, stop_revision=None):
924
 
        """Pull in new perfect-fit revisions."""
925
 
        # FIXME: If the branches have diverged, but the latest
926
 
        # revision in this branch is completely merged into the other,
927
 
        # then we should still be able to pull.
928
 
        from bzrlib.fetch import greedy_fetch
929
 
        if stop_revision is None:
930
 
            stop_revision = other.last_revision()
931
 
        ### Should this be checking is_ancestor instead of revision_history?
932
 
        if (stop_revision is not None and 
933
 
            stop_revision in self.revision_history()):
934
 
            return
935
 
        greedy_fetch(to_branch=self, from_branch=other,
936
 
                     revision=stop_revision)
937
 
        pullable_revs = self.pullable_revisions(other, stop_revision)
938
 
        if len(pullable_revs) > 0:
939
 
            self.append_revision(*pullable_revs)
940
 
 
941
 
    def pullable_revisions(self, other, stop_revision):
942
 
        other_revno = other.revision_id_to_revno(stop_revision)
 
763
        """Pull in all new revisions from other branch.
 
764
        
 
765
        >>> from bzrlib.commit import commit
 
766
        >>> bzrlib.trace.silent = True
 
767
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
768
        >>> br1.add('foo')
 
769
        >>> br1.add('bar')
 
770
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
771
        >>> br2 = ScratchBranch()
 
772
        >>> br2.update_revisions(br1)
 
773
        Added 2 texts.
 
774
        Added 1 inventories.
 
775
        Added 1 revisions.
 
776
        >>> br2.revision_history()
 
777
        [u'REVISION-ID-1']
 
778
        >>> br2.update_revisions(br1)
 
779
        Added 0 texts.
 
780
        Added 0 inventories.
 
781
        Added 0 revisions.
 
782
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
783
        True
 
784
        """
 
785
        from bzrlib.progress import ProgressBar
943
786
        try:
944
 
            return self.missing_revisions(other, other_revno)
945
 
        except DivergedBranches, e:
946
 
            try:
947
 
                pullable_revs = get_intervening_revisions(self.last_revision(),
948
 
                                                          stop_revision, self)
949
 
                assert self.last_revision() not in pullable_revs
950
 
                return pullable_revs
951
 
            except bzrlib.errors.NotAncestor:
952
 
                if is_ancestor(self.last_revision(), stop_revision, self):
953
 
                    return []
954
 
                else:
955
 
                    raise e
 
787
            set
 
788
        except NameError:
 
789
            from sets import Set as set
 
790
 
 
791
        pb = ProgressBar()
 
792
 
 
793
        pb.update('comparing histories')
 
794
        revision_ids = self.missing_revisions(other, stop_revision)
 
795
 
 
796
        if hasattr(other.revision_store, "prefetch"):
 
797
            other.revision_store.prefetch(revision_ids)
 
798
        if hasattr(other.inventory_store, "prefetch"):
 
799
            inventory_ids = [other.get_revision(r).inventory_id
 
800
                             for r in revision_ids]
 
801
            other.inventory_store.prefetch(inventory_ids)
 
802
                
 
803
        revisions = []
 
804
        needed_texts = set()
 
805
        i = 0
 
806
        for rev_id in revision_ids:
 
807
            i += 1
 
808
            pb.update('fetching revision', i, len(revision_ids))
 
809
            rev = other.get_revision(rev_id)
 
810
            revisions.append(rev)
 
811
            inv = other.get_inventory(str(rev.inventory_id))
 
812
            for key, entry in inv.iter_entries():
 
813
                if entry.text_id is None:
 
814
                    continue
 
815
                if entry.text_id not in self.text_store:
 
816
                    needed_texts.add(entry.text_id)
 
817
 
 
818
        pb.clear()
 
819
                    
 
820
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
821
        print "Added %d texts." % count 
 
822
        inventory_ids = [ f.inventory_id for f in revisions ]
 
823
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
824
                                                inventory_ids)
 
825
        print "Added %d inventories." % count 
 
826
        revision_ids = [ f.revision_id for f in revisions]
 
827
        count = self.revision_store.copy_multi(other.revision_store, 
 
828
                                               revision_ids)
 
829
        for revision_id in revision_ids:
 
830
            self.append_revision(revision_id)
 
831
        print "Added %d revisions." % count
 
832
                    
956
833
        
957
834
    def commit(self, *args, **kw):
958
 
        from bzrlib.commit import Commit
959
 
        Commit().commit(self, *args, **kw)
960
 
    
961
 
    def revision_id_to_revno(self, revision_id):
962
 
        """Given a revision id, return its revno"""
963
 
        if revision_id is None:
964
 
            return 0
965
 
        history = self.revision_history()
966
 
        try:
967
 
            return history.index(revision_id) + 1
968
 
        except ValueError:
969
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
835
        from bzrlib.commit import commit
 
836
        commit(self, *args, **kw)
 
837
        
970
838
 
971
 
    def get_rev_id(self, revno, history=None):
972
 
        """Find the revision id of the specified revno."""
 
839
    def lookup_revision(self, revno):
 
840
        """Return revision hash for revision number."""
973
841
        if revno == 0:
974
842
            return None
975
 
        if history is None:
976
 
            history = self.revision_history()
977
 
        elif revno <= 0 or revno > len(history):
978
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
979
 
        return history[revno - 1]
 
843
 
 
844
        try:
 
845
            # list is 0-based; revisions are 1-based
 
846
            return self.revision_history()[revno-1]
 
847
        except IndexError:
 
848
            raise BzrError("no such revision %s" % revno)
 
849
 
980
850
 
981
851
    def revision_tree(self, revision_id):
982
852
        """Return Tree for a revision on this branch.
983
853
 
984
854
        `revision_id` may be None for the null revision, in which case
985
855
        an `EmptyTree` is returned."""
 
856
        from bzrlib.tree import EmptyTree, RevisionTree
986
857
        # TODO: refactor this to use an existing revision object
987
858
        # so we don't need to read it in twice.
988
859
        if revision_id == None:
989
860
            return EmptyTree()
990
861
        else:
991
862
            inv = self.get_revision_inventory(revision_id)
992
 
            return RevisionTree(self.weave_store, inv, revision_id)
 
863
            return RevisionTree(self.text_store, inv)
 
864
 
993
865
 
994
866
    def working_tree(self):
995
867
        """Return a `Tree` for the working copy."""
996
 
        from bzrlib.workingtree import WorkingTree
997
 
        # TODO: In the future, WorkingTree should utilize Transport
998
 
        # RobertCollins 20051003 - I don't think it should - working trees are
999
 
        # much more complex to keep consistent than our careful .bzr subset.
1000
 
        # instead, we should say that working trees are local only, and optimise
1001
 
        # for that.
1002
 
        return WorkingTree(self.base, branch=self)
 
868
        from workingtree import WorkingTree
 
869
        return WorkingTree(self.base, self.read_working_inventory())
1003
870
 
1004
871
 
1005
872
    def basis_tree(self):
1007
874
 
1008
875
        If there are no revisions yet, return an `EmptyTree`.
1009
876
        """
1010
 
        return self.revision_tree(self.last_revision())
 
877
        from bzrlib.tree import EmptyTree, RevisionTree
 
878
        r = self.last_patch()
 
879
        if r == None:
 
880
            return EmptyTree()
 
881
        else:
 
882
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
883
 
1011
884
 
1012
885
 
1013
886
    def rename_one(self, from_rel, to_rel):
1045
918
 
1046
919
            inv.rename(file_id, to_dir_id, to_tail)
1047
920
 
 
921
            print "%s => %s" % (from_rel, to_rel)
 
922
 
1048
923
            from_abs = self.abspath(from_rel)
1049
924
            to_abs = self.abspath(to_rel)
1050
925
            try:
1051
 
                rename(from_abs, to_abs)
 
926
                os.rename(from_abs, to_abs)
1052
927
            except OSError, e:
1053
928
                raise BzrError("failed to rename %r to %r: %s"
1054
929
                        % (from_abs, to_abs, e[1]),
1069
944
 
1070
945
        Note that to_name is only the last component of the new name;
1071
946
        this doesn't change the directory.
1072
 
 
1073
 
        This returns a list of (from_path, to_path) pairs for each
1074
 
        entry that is moved.
1075
947
        """
1076
 
        result = []
1077
948
        self.lock_write()
1078
949
        try:
1079
950
            ## TODO: Option to move IDs only
1114
985
            for f in from_paths:
1115
986
                name_tail = splitpath(f)[-1]
1116
987
                dest_path = appendpath(to_name, name_tail)
1117
 
                result.append((f, dest_path))
 
988
                print "%s => %s" % (f, dest_path)
1118
989
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1119
990
                try:
1120
 
                    rename(self.abspath(f), self.abspath(dest_path))
 
991
                    os.rename(self.abspath(f), self.abspath(dest_path))
1121
992
                except OSError, e:
1122
993
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1123
994
                            ["rename rolled back"])
1126
997
        finally:
1127
998
            self.unlock()
1128
999
 
1129
 
        return result
1130
 
 
1131
1000
 
1132
1001
    def revert(self, filenames, old_tree=None, backups=True):
1133
1002
        """Restore selected files to the versions from a previous tree.
1179
1048
        These are revisions that have been merged into the working
1180
1049
        directory but not yet committed.
1181
1050
        """
1182
 
        cfn = self._rel_controlfilename('pending-merges')
1183
 
        if not self._transport.has(cfn):
 
1051
        cfn = self.controlfilename('pending-merges')
 
1052
        if not os.path.exists(cfn):
1184
1053
            return []
1185
1054
        p = []
1186
1055
        for l in self.controlfile('pending-merges', 'r').readlines():
1188
1057
        return p
1189
1058
 
1190
1059
 
1191
 
    def add_pending_merge(self, *revision_ids):
1192
 
        # TODO: Perhaps should check at this point that the
1193
 
        # history of the revision is actually present?
 
1060
    def add_pending_merge(self, revision_id):
 
1061
        from bzrlib.revision import validate_revision_id
 
1062
 
 
1063
        validate_revision_id(revision_id)
 
1064
 
1194
1065
        p = self.pending_merges()
1195
 
        updated = False
1196
 
        for rev_id in revision_ids:
1197
 
            if rev_id in p:
1198
 
                continue
1199
 
            p.append(rev_id)
1200
 
            updated = True
1201
 
        if updated:
1202
 
            self.set_pending_merges(p)
 
1066
        if revision_id in p:
 
1067
            return
 
1068
        p.append(revision_id)
 
1069
        self.set_pending_merges(p)
 
1070
 
1203
1071
 
1204
1072
    def set_pending_merges(self, rev_list):
1205
 
        self.lock_write()
1206
 
        try:
1207
 
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1208
 
        finally:
1209
 
            self.unlock()
1210
 
 
1211
 
 
1212
 
    def get_parent(self):
1213
 
        """Return the parent location of the branch.
1214
 
 
1215
 
        This is the default location for push/pull/missing.  The usual
1216
 
        pattern is that the user can override it by specifying a
1217
 
        location.
1218
 
        """
1219
 
        import errno
1220
 
        _locs = ['parent', 'pull', 'x-pull']
1221
 
        for l in _locs:
1222
 
            try:
1223
 
                return self.controlfile(l, 'r').read().strip('\n')
1224
 
            except IOError, e:
1225
 
                if e.errno != errno.ENOENT:
1226
 
                    raise
1227
 
        return None
1228
 
 
1229
 
 
1230
 
    def set_parent(self, url):
1231
 
        # TODO: Maybe delete old location files?
1232
1073
        from bzrlib.atomicfile import AtomicFile
1233
1074
        self.lock_write()
1234
1075
        try:
1235
 
            f = AtomicFile(self.controlfilename('parent'))
 
1076
            f = AtomicFile(self.controlfilename('pending-merges'))
1236
1077
            try:
1237
 
                f.write(url + '\n')
 
1078
                for l in rev_list:
 
1079
                    print >>f, l
1238
1080
                f.commit()
1239
1081
            finally:
1240
1082
                f.close()
1241
1083
        finally:
1242
1084
            self.unlock()
1243
1085
 
1244
 
    def check_revno(self, revno):
1245
 
        """\
1246
 
        Check whether a revno corresponds to any revision.
1247
 
        Zero (the NULL revision) is considered valid.
1248
 
        """
1249
 
        if revno != 0:
1250
 
            self.check_real_revno(revno)
1251
 
            
1252
 
    def check_real_revno(self, revno):
1253
 
        """\
1254
 
        Check whether a revno corresponds to a real revision.
1255
 
        Zero (the NULL revision) is considered invalid
1256
 
        """
1257
 
        if revno < 1 or revno > self.revno():
1258
 
            raise InvalidRevisionNumber(revno)
1259
 
        
1260
 
    def sign_revision(self, revision_id, gpg_strategy):
1261
 
        self.lock_write()
1262
 
        try:
1263
 
            plaintext = Testament.from_revision(self, revision_id).as_short_text()
1264
 
            self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
1265
 
                                    revision_id, "sig")
1266
 
        finally:
1267
 
            self.unlock()
1268
 
 
1269
 
 
1270
 
class ScratchBranch(_Branch):
 
1086
 
 
1087
 
 
1088
class ScratchBranch(Branch):
1271
1089
    """Special test class: a branch that cleans up after itself.
1272
1090
 
1273
1091
    >>> b = ScratchBranch()
1274
1092
    >>> isdir(b.base)
1275
1093
    True
1276
1094
    >>> bd = b.base
1277
 
    >>> b._transport.__del__()
 
1095
    >>> b.destroy()
1278
1096
    >>> isdir(bd)
1279
1097
    False
1280
1098
    """
1281
 
 
1282
 
    def __init__(self, files=[], dirs=[], transport=None):
 
1099
    def __init__(self, files=[], dirs=[], base=None):
1283
1100
        """Make a test branch.
1284
1101
 
1285
1102
        This creates a temporary directory and runs init-tree in it.
1286
1103
 
1287
1104
        If any files are listed, they are created in the working copy.
1288
1105
        """
1289
 
        if transport is None:
1290
 
            transport = bzrlib.transport.local.ScratchTransport()
1291
 
            super(ScratchBranch, self).__init__(transport, init=True)
1292
 
        else:
1293
 
            super(ScratchBranch, self).__init__(transport)
1294
 
 
 
1106
        from tempfile import mkdtemp
 
1107
        init = False
 
1108
        if base is None:
 
1109
            base = mkdtemp()
 
1110
            init = True
 
1111
        Branch.__init__(self, base, init=init)
1295
1112
        for d in dirs:
1296
 
            self._transport.mkdir(d)
 
1113
            os.mkdir(self.abspath(d))
1297
1114
            
1298
1115
        for f in files:
1299
 
            self._transport.put(f, 'content of %s' % f)
 
1116
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1300
1117
 
1301
1118
 
1302
1119
    def clone(self):
1303
1120
        """
1304
1121
        >>> orig = ScratchBranch(files=["file1", "file2"])
1305
1122
        >>> clone = orig.clone()
1306
 
        >>> if os.name != 'nt':
1307
 
        ...   os.path.samefile(orig.base, clone.base)
1308
 
        ... else:
1309
 
        ...   orig.base == clone.base
1310
 
        ...
 
1123
        >>> os.path.samefile(orig.base, clone.base)
1311
1124
        False
1312
1125
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1313
1126
        True
1317
1130
        base = mkdtemp()
1318
1131
        os.rmdir(base)
1319
1132
        copytree(self.base, base, symlinks=True)
1320
 
        return ScratchBranch(
1321
 
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1133
        return ScratchBranch(base=base)
 
1134
        
 
1135
    def __del__(self):
 
1136
        self.destroy()
 
1137
 
 
1138
    def destroy(self):
 
1139
        """Destroy the test branch, removing the scratch directory."""
 
1140
        from shutil import rmtree
 
1141
        try:
 
1142
            if self.base:
 
1143
                mutter("delete ScratchBranch %s" % self.base)
 
1144
                rmtree(self.base)
 
1145
        except OSError, e:
 
1146
            # Work around for shutil.rmtree failing on Windows when
 
1147
            # readonly files are encountered
 
1148
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1149
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1150
                for name in files:
 
1151
                    os.chmod(os.path.join(root, name), 0700)
 
1152
            rmtree(self.base)
 
1153
        self.base = None
 
1154
 
1322
1155
    
1323
1156
 
1324
1157
######################################################################
1367
1200
 
1368
1201
    s = hexlify(rand_bytes(8))
1369
1202
    return '-'.join((name, compact_date(time()), s))
1370
 
 
1371
 
 
1372
 
def gen_root_id():
1373
 
    """Return a new tree-root file id."""
1374
 
    return gen_file_id('TREE_ROOT')
1375
 
 
1376