/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: Robert Collins
  • Date: 2005-10-09 23:52:04 UTC
  • Revision ID: robertc@robertcollins.net-20051009235204-0abf18346c658e34
touchup the prefixed-store patch

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