~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-10-03 19:50:36 UTC
  • mfrom: (1399)
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1419.
  • Revision ID: abentley@panoramicfeedback.com-20051003195036-28dbd56f0e852b08
Merged latest from Robert Collins

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import os
20
20
import errno
21
21
from warnings import warn
 
22
from cStringIO import StringIO
22
23
 
23
24
 
24
25
import bzrlib
29
30
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
30
31
                           NoSuchRevision, HistoryMissing, NotBranchError,
31
32
                           DivergedBranches, LockError, UnlistableStore,
32
 
                           UnlistableBranch)
 
33
                           UnlistableBranch, NoSuchFile)
33
34
from bzrlib.textui import show_status
34
35
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
35
36
from bzrlib.delta import compare_trees
36
37
from bzrlib.tree import EmptyTree, RevisionTree
37
38
from bzrlib.inventory import Inventory
38
 
from bzrlib.weavestore import WeaveStore
39
 
from bzrlib.store import copy_all, ImmutableStore
 
39
from bzrlib.store import copy_all
 
40
from bzrlib.store.compressed_text import CompressedTextStore
 
41
from bzrlib.store.text import TextStore
 
42
from bzrlib.store.weave import WeaveStore
 
43
from bzrlib.transport import Transport, get_transport
40
44
import bzrlib.xml5
41
45
import bzrlib.ui
42
46
 
55
59
    # XXX: leave this here for about one release, then remove it
56
60
    raise NotImplementedError('find_branch() is not supported anymore, '
57
61
                              'please use one of the new branch constructors')
58
 
 
59
62
def _relpath(base, path):
60
63
    """Return path relative to base, or raise exception.
61
64
 
83
86
    return os.sep.join(s)
84
87
        
85
88
 
86
 
def find_branch_root(f=None):
87
 
    """Find the branch root enclosing f, or pwd.
88
 
 
89
 
    f may be a filename or a URL.
90
 
 
91
 
    It is not necessary that f exists.
 
89
def find_branch_root(t):
 
90
    """Find the branch root enclosing the transport's base.
 
91
 
 
92
    t is a Transport object.
 
93
 
 
94
    It is not necessary that the base of t exists.
92
95
 
93
96
    Basically we keep looking up until we find the control directory or
94
97
    run into the root.  If there isn't one, raises NotBranchError.
95
98
    """
96
 
    if f == None:
97
 
        f = os.getcwd()
98
 
    elif hasattr(os.path, 'realpath'):
99
 
        f = os.path.realpath(f)
100
 
    else:
101
 
        f = os.path.abspath(f)
102
 
    if not os.path.exists(f):
103
 
        raise BzrError('%r does not exist' % f)
104
 
        
105
 
 
106
 
    orig_f = f
107
 
 
 
99
    orig_base = t.base
108
100
    while True:
109
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
110
 
            return f
111
 
        head, tail = os.path.split(f)
112
 
        if head == f:
 
101
        if t.has(bzrlib.BZRDIR):
 
102
            return t
 
103
        new_t = t.clone('..')
 
104
        if new_t.base == t.base:
113
105
            # reached the root, whatever that may be
114
 
            raise NotBranchError('%s is not in a branch' % orig_f)
115
 
        f = head
116
 
 
117
 
 
 
106
            raise NotBranchError('%s is not in a branch' % orig_base)
 
107
        t = new_t
118
108
 
119
109
 
120
110
######################################################################
136
126
        """Open a branch which may be of an old format.
137
127
        
138
128
        Only local branches are supported."""
139
 
        return LocalBranch(base, find_root=False, relax_version_check=True)
 
129
        return _Branch(get_transport(base), relax_version_check=True)
140
130
        
141
131
    @staticmethod
142
132
    def open(base):
143
133
        """Open an existing branch, rooted at 'base' (url)"""
144
 
        if base and (base.startswith('http://') or base.startswith('https://')):
145
 
            from bzrlib.remotebranch import RemoteBranch
146
 
            return RemoteBranch(base, find_root=False)
147
 
        else:
148
 
            return LocalBranch(base, find_root=False)
 
134
        t = get_transport(base)
 
135
        return _Branch(t)
149
136
 
150
137
    @staticmethod
151
138
    def open_containing(url):
153
140
        
154
141
        This probes for a branch at url, and searches upwards from there.
155
142
        """
156
 
        if url and (url.startswith('http://') or url.startswith('https://')):
157
 
            from bzrlib.remotebranch import RemoteBranch
158
 
            return RemoteBranch(url)
159
 
        else:
160
 
            return LocalBranch(url)
 
143
        t = get_transport(url)
 
144
        t = find_branch_root(t)
 
145
        return _Branch(t)
161
146
 
162
147
    @staticmethod
163
148
    def initialize(base):
164
149
        """Create a new branch, rooted at 'base' (url)"""
165
 
        if base and (base.startswith('http://') or base.startswith('https://')):
166
 
            from bzrlib.remotebranch import RemoteBranch
167
 
            return RemoteBranch(base, init=True)
168
 
        else:
169
 
            return LocalBranch(base, init=True)
 
150
        t = get_transport(base)
 
151
        return _Branch(t, init=True)
170
152
 
171
153
    def setup_caching(self, cache_root):
172
154
        """Subclasses that care about caching should override this, and set
174
156
        """
175
157
 
176
158
 
177
 
class LocalBranch(Branch):
 
159
class _Branch(Branch):
178
160
    """A branch stored in the actual filesystem.
179
161
 
180
162
    Note that it's "local" in the context of the filesystem; it doesn't
225
207
        except UnlistableStore:
226
208
            raise UnlistableBranch(from_store)
227
209
 
228
 
    def __init__(self, base, init=False, find_root=True,
 
210
    def __init__(self, transport, init=False,
229
211
                 relax_version_check=False):
230
212
        """Create new branch object at a particular location.
231
213
 
232
 
        base -- Base directory for the branch. May be a file:// url.
 
214
        transport -- A Transport object, defining how to access files.
 
215
                (If a string, transport.transport() will be used to
 
216
                create a Transport object)
233
217
        
234
218
        init -- If True, create new control files in a previously
235
219
             unversioned directory.  If False, the branch must already
236
220
             be versioned.
237
221
 
238
 
        find_root -- If true and init is false, find the root of the
239
 
             existing branch containing base.
240
 
 
241
222
        relax_version_check -- If true, the usual check for the branch
242
223
            version is not applied.  This is intended only for
243
224
            upgrade/recovery type use; it's not guaranteed that
246
227
        In the test suite, creation of new trees is tested using the
247
228
        `ScratchBranch` class.
248
229
        """
 
230
        assert isinstance(transport, Transport), \
 
231
            "%r is not a Transport" % transport
 
232
        self._transport = transport
249
233
        if init:
250
 
            self.base = os.path.realpath(base)
251
234
            self._make_control()
252
 
        elif find_root:
253
 
            self.base = find_branch_root(base)
254
 
        else:
255
 
            if base.startswith("file://"):
256
 
                base = base[7:]
257
 
            self.base = os.path.realpath(base)
258
 
            if not isdir(self.controlfilename('.')):
259
 
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
260
 
                                     ['use "bzr init" to initialize a '
261
 
                                      'new working tree'])
262
235
        self._check_format(relax_version_check)
263
 
        cfn = self.controlfilename
 
236
 
 
237
        def get_store(name, compressed=True):
 
238
            relpath = self._rel_controlfilename(name)
 
239
            if compressed:
 
240
                store = CompressedTextStore(self._transport.clone(relpath))
 
241
            else:
 
242
                store = TextStore(self._transport.clone(relpath))
 
243
            if self._transport.should_cache():
 
244
                from meta_store import CachedStore
 
245
                cache_path = os.path.join(self.cache_root, name)
 
246
                os.mkdir(cache_path)
 
247
                store = CachedStore(store, cache_path)
 
248
            return store
 
249
        def get_weave(name):
 
250
            relpath = self._rel_controlfilename(name)
 
251
            ws = WeaveStore(self._transport.clone(relpath))
 
252
            if self._transport.should_cache():
 
253
                ws.enable_cache = True
 
254
            return ws
 
255
 
264
256
        if self._branch_format == 4:
265
 
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
266
 
            self.text_store = ImmutableStore(cfn('text-store'))
 
257
            self.inventory_store = get_store('inventory-store')
 
258
            self.text_store = get_store('text-store')
 
259
            self.revision_store = get_store('revision-store')
267
260
        elif self._branch_format == 5:
268
 
            self.control_weaves = WeaveStore(cfn([]))
269
 
            self.weave_store = WeaveStore(cfn('weaves'))
270
 
            if init:
271
 
                # FIXME: Unify with make_control_files
272
 
                self.control_weaves.put_empty_weave('inventory')
273
 
                self.control_weaves.put_empty_weave('ancestry')
274
 
        self.revision_store = ImmutableStore(cfn('revision-store'))
275
 
 
 
261
            self.control_weaves = get_weave([])
 
262
            self.weave_store = get_weave('weaves')
 
263
            self.revision_store = get_store('revision-store', compressed=False)
276
264
 
277
265
    def __str__(self):
278
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
266
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
279
267
 
280
268
 
281
269
    __repr__ = __str__
288
276
            warn("branch %r was not explicitly unlocked" % self)
289
277
            self._lock.unlock()
290
278
 
 
279
        # TODO: It might be best to do this somewhere else,
 
280
        # but it is nice for a Branch object to automatically
 
281
        # cache it's information.
 
282
        # Alternatively, we could have the Transport objects cache requests
 
283
        # See the earlier discussion about how major objects (like Branch)
 
284
        # should never expect their __del__ function to run.
 
285
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
286
            try:
 
287
                import shutil
 
288
                shutil.rmtree(self.cache_root)
 
289
            except:
 
290
                pass
 
291
            self.cache_root = None
 
292
 
 
293
    def _get_base(self):
 
294
        if self._transport:
 
295
            return self._transport.base
 
296
        return None
 
297
 
 
298
    base = property(_get_base)
 
299
 
 
300
 
291
301
    def lock_write(self):
 
302
        # TODO: Upgrade locking to support using a Transport,
 
303
        # and potentially a remote locking protocol
292
304
        if self._lock_mode:
293
305
            if self._lock_mode != 'w':
294
306
                raise LockError("can't upgrade to a write lock from %r" %
295
307
                                self._lock_mode)
296
308
            self._lock_count += 1
297
309
        else:
298
 
            from bzrlib.lock import WriteLock
299
 
 
300
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
310
            self._lock = self._transport.lock_write(
 
311
                    self._rel_controlfilename('branch-lock'))
301
312
            self._lock_mode = 'w'
302
313
            self._lock_count = 1
303
314
 
308
319
                   "invalid lock mode %r" % self._lock_mode
309
320
            self._lock_count += 1
310
321
        else:
311
 
            from bzrlib.lock import ReadLock
312
 
 
313
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
322
            self._lock = self._transport.lock_read(
 
323
                    self._rel_controlfilename('branch-lock'))
314
324
            self._lock_mode = 'r'
315
325
            self._lock_count = 1
316
326
                        
327
337
 
328
338
    def abspath(self, name):
329
339
        """Return absolute filename for something in the branch"""
330
 
        return os.path.join(self.base, name)
 
340
        return self._transport.abspath(name)
331
341
 
332
342
    def relpath(self, path):
333
343
        """Return path relative to this branch of something inside it.
334
344
 
335
345
        Raises an error if path is not in this branch."""
336
 
        return _relpath(self.base, path)
 
346
        return self._transport.relpath(path)
 
347
 
 
348
 
 
349
    def _rel_controlfilename(self, file_or_path):
 
350
        if isinstance(file_or_path, basestring):
 
351
            file_or_path = [file_or_path]
 
352
        return [bzrlib.BZRDIR] + file_or_path
337
353
 
338
354
    def controlfilename(self, file_or_path):
339
355
        """Return location relative to branch."""
340
 
        if isinstance(file_or_path, basestring):
341
 
            file_or_path = [file_or_path]
342
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
356
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
343
357
 
344
358
 
345
359
    def controlfile(self, file_or_path, mode='r'):
353
367
        Controlfiles should almost never be opened in write mode but
354
368
        rather should be atomically copied and replaced using atomicfile.
355
369
        """
356
 
 
357
 
        fn = self.controlfilename(file_or_path)
358
 
 
359
 
        if mode == 'rb' or mode == 'wb':
360
 
            return file(fn, mode)
361
 
        elif mode == 'r' or mode == 'w':
362
 
            # open in binary mode anyhow so there's no newline translation;
363
 
            # codecs uses line buffering by default; don't want that.
364
 
            import codecs
365
 
            return codecs.open(fn, mode + 'b', 'utf-8',
366
 
                               buffering=60000)
 
370
        import codecs
 
371
 
 
372
        relpath = self._rel_controlfilename(file_or_path)
 
373
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
374
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
375
        if mode == 'rb': 
 
376
            return self._transport.get(relpath)
 
377
        elif mode == 'wb':
 
378
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
379
        elif mode == 'r':
 
380
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
381
        elif mode == 'w':
 
382
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
367
383
        else:
368
384
            raise BzrError("invalid controlfile mode %r" % mode)
369
385
 
 
386
    def put_controlfile(self, path, f, encode=True):
 
387
        """Write an entry as a controlfile.
 
388
 
 
389
        :param path: The path to put the file, relative to the .bzr control
 
390
                     directory
 
391
        :param f: A file-like or string object whose contents should be copied.
 
392
        :param encode:  If true, encode the contents as utf-8
 
393
        """
 
394
        self.put_controlfiles([(path, f)], encode=encode)
 
395
 
 
396
    def put_controlfiles(self, files, encode=True):
 
397
        """Write several entries as controlfiles.
 
398
 
 
399
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
400
                      underneath the bzr control directory
 
401
        :param encode:  If true, encode the contents as utf-8
 
402
        """
 
403
        import codecs
 
404
        ctrl_files = []
 
405
        for path, f in files:
 
406
            if encode:
 
407
                if isinstance(f, basestring):
 
408
                    f = f.encode('utf-8', 'replace')
 
409
                else:
 
410
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
411
            path = self._rel_controlfilename(path)
 
412
            ctrl_files.append((path, f))
 
413
        self._transport.put_multi(ctrl_files)
 
414
 
370
415
    def _make_control(self):
371
 
        os.mkdir(self.controlfilename([]))
372
 
        self.controlfile('README', 'w').write(
373
 
            "This is a Bazaar-NG control directory.\n"
374
 
            "Do not change any files in this directory.\n")
375
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
376
 
        for d in ('text-store', 'revision-store',
377
 
                  'weaves'):
378
 
            os.mkdir(self.controlfilename(d))
379
 
        for f in ('revision-history',
380
 
                  'branch-name',
381
 
                  'branch-lock',
382
 
                  'pending-merges'):
383
 
            self.controlfile(f, 'w').write('')
384
 
        mutter('created control directory in ' + self.base)
385
 
 
 
416
        from bzrlib.inventory import Inventory
 
417
        from bzrlib.weavefile import write_weave_v5
 
418
        from bzrlib.weave import Weave
 
419
        
 
420
        # Create an empty inventory
 
421
        sio = StringIO()
386
422
        # if we want per-tree root ids then this is the place to set
387
423
        # them; they're not needed for now and so ommitted for
388
424
        # simplicity.
389
 
        f = self.controlfile('inventory','w')
390
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
425
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
426
        empty_inv = sio.getvalue()
 
427
        sio = StringIO()
 
428
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
429
        empty_weave = sio.getvalue()
391
430
 
 
431
        dirs = [[], 'revision-store', 'weaves']
 
432
        files = [('README', 
 
433
            "This is a Bazaar-NG control directory.\n"
 
434
            "Do not change any files in this directory.\n"),
 
435
            ('branch-format', BZR_BRANCH_FORMAT_5),
 
436
            ('revision-history', ''),
 
437
            ('branch-name', ''),
 
438
            ('branch-lock', ''),
 
439
            ('pending-merges', ''),
 
440
            ('inventory', empty_inv),
 
441
            ('inventory.weave', empty_weave),
 
442
            ('ancestry.weave', empty_weave)
 
443
        ]
 
444
        cfn = self._rel_controlfilename
 
445
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
446
        self.put_controlfiles(files)
 
447
        mutter('created control directory in ' + self._transport.base)
392
448
 
393
449
    def _check_format(self, relax_version_check):
394
450
        """Check this branch format is supported.
401
457
        """
402
458
        try:
403
459
            fmt = self.controlfile('branch-format', 'r').read()
404
 
        except IOError, e:
405
 
            if e.errno == errno.ENOENT:
406
 
                raise NotBranchError(self.base)
407
 
            else:
408
 
                raise
 
460
        except NoSuchFile:
 
461
            raise NotBranchError(self.base)
409
462
 
410
463
        if fmt == BZR_BRANCH_FORMAT_5:
411
464
            self._branch_format = 5
416
469
            and self._branch_format != 5):
417
470
            raise BzrError('sorry, branch format %r not supported' % fmt,
418
471
                           ['use a different bzr version',
419
 
                            'or remove the .bzr directory and "bzr init" again'])
 
472
                            'or remove the .bzr directory'
 
473
                            ' and "bzr init" again'])
420
474
 
421
475
    def get_root_id(self):
422
476
        """Return the id of this branches root"""
453
507
        That is to say, the inventory describing changes underway, that
454
508
        will be committed to the next revision.
455
509
        """
456
 
        from bzrlib.atomicfile import AtomicFile
457
 
        
 
510
        from cStringIO import StringIO
458
511
        self.lock_write()
459
512
        try:
460
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
461
 
            try:
462
 
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
463
 
                f.commit()
464
 
            finally:
465
 
                f.close()
 
513
            sio = StringIO()
 
514
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
515
            sio.seek(0)
 
516
            # Transport handles atomicity
 
517
            self.put_controlfile('inventory', sio)
466
518
        finally:
467
519
            self.unlock()
468
520
        
469
521
        mutter('wrote working inventory')
470
522
            
471
 
 
472
523
    inventory = property(read_working_inventory, _write_inventory, None,
473
524
                         """Inventory for the working copy.""")
474
525
 
475
 
 
476
526
    def add(self, files, ids=None):
477
527
        """Make files versioned.
478
528
 
526
576
                    kind = file_kind(fullpath)
527
577
                except OSError:
528
578
                    # maybe something better?
529
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
579
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
530
580
 
531
 
                if kind != 'file' and kind != 'directory':
532
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
581
                if kind not in ('file', 'directory', 'symlink'):
 
582
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
533
583
 
534
584
                if file_id is None:
535
585
                    file_id = gen_file_id(f)
600
650
        finally:
601
651
            self.unlock()
602
652
 
603
 
 
604
653
    # FIXME: this doesn't need to be a branch method
605
654
    def set_inventory(self, new_inventory_list):
606
655
        from bzrlib.inventory import Inventory, InventoryEntry
612
661
            inv.add(InventoryEntry(file_id, name, kind, parent))
613
662
        self._write_inventory(inv)
614
663
 
615
 
 
616
664
    def unknowns(self):
617
665
        """Return all unknown files.
618
666
 
633
681
 
634
682
 
635
683
    def append_revision(self, *revision_ids):
636
 
        from bzrlib.atomicfile import AtomicFile
637
 
 
638
684
        for revision_id in revision_ids:
639
685
            mutter("add {%s} to revision-history" % revision_id)
640
 
 
641
 
        rev_history = self.revision_history()
642
 
        rev_history.extend(revision_ids)
643
 
 
644
 
        f = AtomicFile(self.controlfilename('revision-history'))
 
686
        self.lock_write()
645
687
        try:
646
 
            for rev_id in rev_history:
647
 
                print >>f, rev_id
648
 
            f.commit()
 
688
            rev_history = self.revision_history()
 
689
            rev_history.extend(revision_ids)
 
690
            self.put_controlfile('revision-history', '\n'.join(rev_history))
649
691
        finally:
650
 
            f.close()
651
 
 
 
692
            self.unlock()
652
693
 
653
694
    def has_revision(self, revision_id):
654
695
        """True if this branch has a copy of the revision.
658
699
        return (revision_id is None
659
700
                or revision_id in self.revision_store)
660
701
 
661
 
 
662
702
    def get_revision_xml_file(self, revision_id):
663
703
        """Return XML file object for revision object."""
664
704
        if not revision_id or not isinstance(revision_id, basestring):
673
713
        finally:
674
714
            self.unlock()
675
715
 
 
716
    #deprecated
 
717
    get_revision_xml = get_revision_xml_file
676
718
 
677
719
    def get_revision_xml(self, revision_id):
678
720
        return self.get_revision_xml_file(revision_id).read()
692
734
        assert r.revision_id == revision_id
693
735
        return r
694
736
 
695
 
 
696
737
    def get_revision_delta(self, revno):
697
738
        """Return the delta for one revision.
698
739
 
714
755
 
715
756
        return compare_trees(old_tree, new_tree)
716
757
 
717
 
 
718
758
    def get_revision_sha1(self, revision_id):
719
759
        """Hash the stored value of a revision, and return it."""
 
760
        # In the future, revision entries will be signed. At that
 
761
        # point, it is probably best *not* to include the signature
 
762
        # in the revision hash. Because that lets you re-sign
 
763
        # the revision, (add signatures/remove signatures) and still
 
764
        # have all hash pointers stay consistent.
 
765
        # But for now, just hash the contents.
720
766
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
721
767
 
722
 
 
723
768
    def _get_ancestry_weave(self):
724
769
        return self.control_weaves.get_weave('ancestry')
725
 
        
726
770
 
727
771
    def get_ancestry(self, revision_id):
728
772
        """Return a list of revision-ids integrated by a revision.
733
777
        w = self._get_ancestry_weave()
734
778
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
735
779
 
736
 
 
737
780
    def get_inventory_weave(self):
738
781
        return self.control_weaves.get_weave('inventory')
739
782
 
740
 
 
741
783
    def get_inventory(self, revision_id):
742
784
        """Get Inventory object by hash."""
743
785
        xml = self.get_inventory_xml(revision_id)
744
786
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
745
787
 
746
 
 
747
788
    def get_inventory_xml(self, revision_id):
748
789
        """Get inventory XML as a file object."""
749
790
        try:
753
794
        except IndexError:
754
795
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
755
796
 
756
 
 
757
797
    def get_inventory_sha1(self, revision_id):
758
798
        """Return the sha1 hash of the inventory entry
759
799
        """
760
800
        return self.get_revision(revision_id).inventory_sha1
761
801
 
762
 
 
763
802
    def get_revision_inventory(self, revision_id):
764
803
        """Return inventory of a past revision."""
765
804
        # TODO: Unify this with get_inventory()
770
809
        else:
771
810
            return self.get_inventory(revision_id)
772
811
 
773
 
 
774
812
    def revision_history(self):
775
813
        """Return sequence of revision hashes on to this branch."""
776
814
        self.lock_read()
780
818
        finally:
781
819
            self.unlock()
782
820
 
783
 
 
784
821
    def common_ancestor(self, other, self_revno=None, other_revno=None):
785
822
        """
786
823
        >>> from bzrlib.commit import commit
951
988
    def working_tree(self):
952
989
        """Return a `Tree` for the working copy."""
953
990
        from bzrlib.workingtree import WorkingTree
954
 
        return WorkingTree(self.base, self.read_working_inventory())
 
991
        # TODO: In the future, WorkingTree should utilize Transport
 
992
        return WorkingTree(self._transport.base, self.read_working_inventory())
955
993
 
956
994
 
957
995
    def basis_tree(self):
1131
1169
        These are revisions that have been merged into the working
1132
1170
        directory but not yet committed.
1133
1171
        """
1134
 
        cfn = self.controlfilename('pending-merges')
1135
 
        if not os.path.exists(cfn):
 
1172
        cfn = self._rel_controlfilename('pending-merges')
 
1173
        if not self._transport.has(cfn):
1136
1174
            return []
1137
1175
        p = []
1138
1176
        for l in self.controlfile('pending-merges', 'r').readlines():
1140
1178
        return p
1141
1179
 
1142
1180
 
1143
 
    def add_pending_merge(self, revision_id):
1144
 
        validate_revision_id(revision_id)
 
1181
    def add_pending_merge(self, *revision_ids):
1145
1182
        # TODO: Perhaps should check at this point that the
1146
1183
        # history of the revision is actually present?
 
1184
        for rev_id in revision_ids:
 
1185
            validate_revision_id(rev_id)
 
1186
 
1147
1187
        p = self.pending_merges()
1148
 
        if revision_id in p:
1149
 
            return
1150
 
        p.append(revision_id)
1151
 
        self.set_pending_merges(p)
1152
 
 
 
1188
        updated = False
 
1189
        for rev_id in revision_ids:
 
1190
            if rev_id in p:
 
1191
                continue
 
1192
            p.append(rev_id)
 
1193
            updated = True
 
1194
        if updated:
 
1195
            self.set_pending_merges(p)
1153
1196
 
1154
1197
    def set_pending_merges(self, rev_list):
1155
 
        from bzrlib.atomicfile import AtomicFile
1156
1198
        self.lock_write()
1157
1199
        try:
1158
 
            f = AtomicFile(self.controlfilename('pending-merges'))
1159
 
            try:
1160
 
                for l in rev_list:
1161
 
                    print >>f, l
1162
 
                f.commit()
1163
 
            finally:
1164
 
                f.close()
 
1200
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1165
1201
        finally:
1166
1202
            self.unlock()
1167
1203
 
1218
1254
        
1219
1255
 
1220
1256
 
1221
 
class ScratchBranch(LocalBranch):
 
1257
class ScratchBranch(_Branch):
1222
1258
    """Special test class: a branch that cleans up after itself.
1223
1259
 
1224
1260
    >>> b = ScratchBranch()
1241
1277
        if base is None:
1242
1278
            base = mkdtemp()
1243
1279
            init = True
1244
 
        LocalBranch.__init__(self, base, init=init)
 
1280
        if isinstance(base, basestring):
 
1281
            base = get_transport(base)
 
1282
        _Branch.__init__(self, base, init=init)
1245
1283
        for d in dirs:
1246
 
            os.mkdir(self.abspath(d))
 
1284
            self._transport.mkdir(d)
1247
1285
            
1248
1286
        for f in files:
1249
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1287
            self._transport.put(f, 'content of %s' % f)
1250
1288
 
1251
1289
 
1252
1290
    def clone(self):
1269
1307
        copytree(self.base, base, symlinks=True)
1270
1308
        return ScratchBranch(base=base)
1271
1309
 
1272
 
 
1273
 
        
1274
1310
    def __del__(self):
1275
1311
        self.destroy()
1276
1312
 
1289
1325
                for name in files:
1290
1326
                    os.chmod(os.path.join(root, name), 0700)
1291
1327
            rmtree(self.base)
1292
 
        self.base = None
 
1328
        self._transport = None
1293
1329
 
1294
1330
    
1295
1331