/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Jelmer Vernooij
  • Date: 2005-10-18 18:13:06 UTC
  • mto: (1185.16.102)
  • mto: This revision was merged to the branch mainline in revision 1488.
  • Revision ID: jelmer@samba.org-20051018181306-96bd114e1f43f112
Properly import exception.

Show diffs side-by-side

added added

removed removed

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