/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-03 14:28:37 UTC
  • mto: (1393.1.30)
  • mto: This revision was merged to the branch mainline in revision 1400.
  • Revision ID: robertc@robertcollins.net-20051003142837-78bf906d4edcbd62
factor out inventory directory logic into 'InventoryDirectory' class

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