~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-09-29 02:01:49 UTC
  • Revision ID: robertc@robertcollins.net-20050929020149-1ff16722c6a01b2c
reenable remotebranch tests

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
23
22
 
24
23
 
25
24
import bzrlib
26
 
from bzrlib.inventory import InventoryEntry
27
 
import bzrlib.inventory as inventory
28
25
from bzrlib.trace import mutter, note
29
26
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
30
27
                            rename, splitpath, sha_file, appendpath, 
32
29
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
33
30
                           NoSuchRevision, HistoryMissing, NotBranchError,
34
31
                           DivergedBranches, LockError, UnlistableStore,
35
 
                           UnlistableBranch, NoSuchFile)
 
32
                           UnlistableBranch)
36
33
from bzrlib.textui import show_status
37
 
from bzrlib.revision import Revision
 
34
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
38
35
from bzrlib.delta import compare_trees
39
36
from bzrlib.tree import EmptyTree, RevisionTree
40
37
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
 
38
from bzrlib.weavestore import WeaveStore
 
39
from bzrlib.store import copy_all, ImmutableStore
46
40
import bzrlib.xml5
47
41
import bzrlib.ui
48
42
 
61
55
    # XXX: leave this here for about one release, then remove it
62
56
    raise NotImplementedError('find_branch() is not supported anymore, '
63
57
                              'please use one of the new branch constructors')
 
58
 
64
59
def _relpath(base, path):
65
60
    """Return path relative to base, or raise exception.
66
61
 
88
83
    return os.sep.join(s)
89
84
        
90
85
 
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.
 
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.
97
92
 
98
93
    Basically we keep looking up until we find the control directory or
99
94
    run into the root.  If there isn't one, raises NotBranchError.
100
95
    """
101
 
    orig_base = t.base
 
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
 
102
108
    while True:
103
 
        if t.has(bzrlib.BZRDIR):
104
 
            return t
105
 
        new_t = t.clone('..')
106
 
        if new_t.base == t.base:
 
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:
107
113
            # reached the root, whatever that may be
108
 
            raise NotBranchError('%s is not in a branch' % orig_base)
109
 
        t = new_t
 
114
            raise NotBranchError('%s is not in a branch' % orig_f)
 
115
        f = head
 
116
 
 
117
 
110
118
 
111
119
 
112
120
######################################################################
124
132
        raise NotImplementedError('The Branch class is abstract')
125
133
 
126
134
    @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
135
    def open(base):
135
136
        """Open an existing branch, rooted at 'base' (url)"""
136
 
        t = get_transport(base)
137
 
        mutter("trying to open %r with transport %r", base, t)
138
 
        return _Branch(t)
 
137
        if base and (base.startswith('http://') or base.startswith('https://')):
 
138
            from bzrlib.remotebranch import RemoteBranch
 
139
            return RemoteBranch(base, find_root=False)
 
140
        else:
 
141
            return LocalBranch(base, find_root=False)
139
142
 
140
143
    @staticmethod
141
144
    def open_containing(url):
143
146
        
144
147
        This probes for a branch at url, and searches upwards from there.
145
148
        """
146
 
        t = get_transport(url)
147
 
        t = find_branch_root(t)
148
 
        return _Branch(t)
 
149
        if url and (url.startswith('http://') or url.startswith('https://')):
 
150
            from bzrlib.remotebranch import RemoteBranch
 
151
            return RemoteBranch(url)
 
152
        else:
 
153
            return LocalBranch(url)
149
154
 
150
155
    @staticmethod
151
156
    def initialize(base):
152
157
        """Create a new branch, rooted at 'base' (url)"""
153
 
        t = get_transport(base)
154
 
        return _Branch(t, init=True)
 
158
        if base and (base.startswith('http://') or base.startswith('https://')):
 
159
            from bzrlib.remotebranch import RemoteBranch
 
160
            return RemoteBranch(base, init=True)
 
161
        else:
 
162
            return LocalBranch(base, init=True)
155
163
 
156
164
    def setup_caching(self, cache_root):
157
165
        """Subclasses that care about caching should override this, and set
158
166
        up cached stores located under cache_root.
159
167
        """
160
 
        self.cache_root = cache_root
161
 
 
162
 
 
163
 
class _Branch(Branch):
 
168
 
 
169
 
 
170
class LocalBranch(Branch):
164
171
    """A branch stored in the actual filesystem.
165
172
 
166
173
    Note that it's "local" in the context of the filesystem; it doesn't
211
218
        except UnlistableStore:
212
219
            raise UnlistableBranch(from_store)
213
220
 
214
 
    def __init__(self, transport, init=False,
 
221
    def __init__(self, base, init=False, find_root=True,
215
222
                 relax_version_check=False):
216
223
        """Create new branch object at a particular location.
217
224
 
218
 
        transport -- A Transport object, defining how to access files.
219
 
                (If a string, transport.transport() will be used to
220
 
                create a Transport object)
 
225
        base -- Base directory for the branch. May be a file:// url.
221
226
        
222
227
        init -- If True, create new control files in a previously
223
228
             unversioned directory.  If False, the branch must already
224
229
             be versioned.
225
230
 
 
231
        find_root -- If true and init is false, find the root of the
 
232
             existing branch containing base.
 
233
 
226
234
        relax_version_check -- If true, the usual check for the branch
227
235
            version is not applied.  This is intended only for
228
236
            upgrade/recovery type use; it's not guaranteed that
231
239
        In the test suite, creation of new trees is tested using the
232
240
        `ScratchBranch` class.
233
241
        """
234
 
        assert isinstance(transport, Transport), \
235
 
            "%r is not a Transport" % transport
236
 
        self._transport = transport
237
242
        if init:
 
243
            self.base = os.path.realpath(base)
238
244
            self._make_control()
 
245
        elif find_root:
 
246
            self.base = find_branch_root(base)
 
247
        else:
 
248
            if base.startswith("file://"):
 
249
                base = base[7:]
 
250
            self.base = os.path.realpath(base)
 
251
            if not isdir(self.controlfilename('.')):
 
252
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
253
                                     ['use "bzr init" to initialize a '
 
254
                                      'new working tree'])
239
255
        self._check_format(relax_version_check)
240
 
 
241
 
        def get_store(name, compressed=True):
242
 
            # FIXME: This approach of assuming stores are all entirely compressed
243
 
            # or entirely uncompressed is tidy, but breaks upgrade from 
244
 
            # some existing branches where there's a mixture; we probably 
245
 
            # still want the option to look for both.
246
 
            relpath = self._rel_controlfilename(name)
247
 
            if compressed:
248
 
                store = CompressedTextStore(self._transport.clone(relpath))
249
 
            else:
250
 
                store = TextStore(self._transport.clone(relpath))
251
 
            #if self._transport.should_cache():
252
 
            #    cache_path = os.path.join(self.cache_root, name)
253
 
            #    os.mkdir(cache_path)
254
 
            #    store = bzrlib.store.CachedStore(store, cache_path)
255
 
            return store
256
 
        def get_weave(name):
257
 
            relpath = self._rel_controlfilename(name)
258
 
            ws = WeaveStore(self._transport.clone(relpath))
259
 
            if self._transport.should_cache():
260
 
                ws.enable_cache = True
261
 
            return ws
262
 
 
 
256
        cfn = self.controlfilename
263
257
        if self._branch_format == 4:
264
 
            self.inventory_store = get_store('inventory-store')
265
 
            self.text_store = get_store('text-store')
266
 
            self.revision_store = get_store('revision-store')
 
258
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
 
259
            self.text_store = ImmutableStore(cfn('text-store'))
267
260
        elif self._branch_format == 5:
268
 
            self.control_weaves = get_weave([])
269
 
            self.weave_store = get_weave('weaves')
270
 
            self.revision_store = get_store('revision-store', compressed=False)
 
261
            self.control_weaves = WeaveStore(cfn([]))
 
262
            self.weave_store = WeaveStore(cfn('weaves'))
 
263
            if init:
 
264
                # FIXME: Unify with make_control_files
 
265
                self.control_weaves.put_empty_weave('inventory')
 
266
                self.control_weaves.put_empty_weave('ancestry')
 
267
        self.revision_store = ImmutableStore(cfn('revision-store'))
 
268
 
271
269
 
272
270
    def __str__(self):
273
 
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
 
271
        return '%s(%r)' % (self.__class__.__name__, self.base)
274
272
 
275
273
 
276
274
    __repr__ = __str__
283
281
            warn("branch %r was not explicitly unlocked" % self)
284
282
            self._lock.unlock()
285
283
 
286
 
        # TODO: It might be best to do this somewhere else,
287
 
        # but it is nice for a Branch object to automatically
288
 
        # cache it's information.
289
 
        # Alternatively, we could have the Transport objects cache requests
290
 
        # See the earlier discussion about how major objects (like Branch)
291
 
        # should never expect their __del__ function to run.
292
 
        if hasattr(self, 'cache_root') and self.cache_root is not None:
293
 
            try:
294
 
                import shutil
295
 
                shutil.rmtree(self.cache_root)
296
 
            except:
297
 
                pass
298
 
            self.cache_root = None
299
 
 
300
 
    def _get_base(self):
301
 
        if self._transport:
302
 
            return self._transport.base
303
 
        return None
304
 
 
305
 
    base = property(_get_base)
306
 
 
307
 
 
308
284
    def lock_write(self):
309
 
        # TODO: Upgrade locking to support using a Transport,
310
 
        # and potentially a remote locking protocol
311
285
        if self._lock_mode:
312
286
            if self._lock_mode != 'w':
313
287
                raise LockError("can't upgrade to a write lock from %r" %
314
288
                                self._lock_mode)
315
289
            self._lock_count += 1
316
290
        else:
317
 
            self._lock = self._transport.lock_write(
318
 
                    self._rel_controlfilename('branch-lock'))
 
291
            from bzrlib.lock import WriteLock
 
292
 
 
293
            self._lock = WriteLock(self.controlfilename('branch-lock'))
319
294
            self._lock_mode = 'w'
320
295
            self._lock_count = 1
321
296
 
326
301
                   "invalid lock mode %r" % self._lock_mode
327
302
            self._lock_count += 1
328
303
        else:
329
 
            self._lock = self._transport.lock_read(
330
 
                    self._rel_controlfilename('branch-lock'))
 
304
            from bzrlib.lock import ReadLock
 
305
 
 
306
            self._lock = ReadLock(self.controlfilename('branch-lock'))
331
307
            self._lock_mode = 'r'
332
308
            self._lock_count = 1
333
309
                        
344
320
 
345
321
    def abspath(self, name):
346
322
        """Return absolute filename for something in the branch"""
347
 
        return self._transport.abspath(name)
 
323
        return os.path.join(self.base, name)
348
324
 
349
325
    def relpath(self, path):
350
326
        """Return path relative to this branch of something inside it.
351
327
 
352
328
        Raises an error if path is not in this branch."""
353
 
        return self._transport.relpath(path)
354
 
 
355
 
 
356
 
    def _rel_controlfilename(self, file_or_path):
 
329
        return _relpath(self.base, path)
 
330
 
 
331
    def controlfilename(self, file_or_path):
 
332
        """Return location relative to branch."""
357
333
        if isinstance(file_or_path, basestring):
358
334
            file_or_path = [file_or_path]
359
 
        return [bzrlib.BZRDIR] + file_or_path
360
 
 
361
 
    def controlfilename(self, file_or_path):
362
 
        """Return location relative to branch."""
363
 
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
 
335
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
364
336
 
365
337
 
366
338
    def controlfile(self, file_or_path, mode='r'):
374
346
        Controlfiles should almost never be opened in write mode but
375
347
        rather should be atomically copied and replaced using atomicfile.
376
348
        """
377
 
        import codecs
378
 
 
379
 
        relpath = self._rel_controlfilename(file_or_path)
380
 
        #TODO: codecs.open() buffers linewise, so it was overloaded with
381
 
        # a much larger buffer, do we need to do the same for getreader/getwriter?
382
 
        if mode == 'rb': 
383
 
            return self._transport.get(relpath)
384
 
        elif mode == 'wb':
385
 
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
386
 
        elif mode == 'r':
387
 
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
388
 
        elif mode == 'w':
389
 
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
 
349
 
 
350
        fn = self.controlfilename(file_or_path)
 
351
 
 
352
        if mode == 'rb' or mode == 'wb':
 
353
            return file(fn, mode)
 
354
        elif mode == 'r' or mode == 'w':
 
355
            # open in binary mode anyhow so there's no newline translation;
 
356
            # codecs uses line buffering by default; don't want that.
 
357
            import codecs
 
358
            return codecs.open(fn, mode + 'b', 'utf-8',
 
359
                               buffering=60000)
390
360
        else:
391
361
            raise BzrError("invalid controlfile mode %r" % mode)
392
362
 
393
 
    def put_controlfile(self, path, f, encode=True):
394
 
        """Write an entry as a controlfile.
395
 
 
396
 
        :param path: The path to put the file, relative to the .bzr control
397
 
                     directory
398
 
        :param f: A file-like or string object whose contents should be copied.
399
 
        :param encode:  If true, encode the contents as utf-8
400
 
        """
401
 
        self.put_controlfiles([(path, f)], encode=encode)
402
 
 
403
 
    def put_controlfiles(self, files, encode=True):
404
 
        """Write several entries as controlfiles.
405
 
 
406
 
        :param files: A list of [(path, file)] pairs, where the path is the directory
407
 
                      underneath the bzr control directory
408
 
        :param encode:  If true, encode the contents as utf-8
409
 
        """
410
 
        import codecs
411
 
        ctrl_files = []
412
 
        for path, f in files:
413
 
            if encode:
414
 
                if isinstance(f, basestring):
415
 
                    f = f.encode('utf-8', 'replace')
416
 
                else:
417
 
                    f = codecs.getwriter('utf-8')(f, errors='replace')
418
 
            path = self._rel_controlfilename(path)
419
 
            ctrl_files.append((path, f))
420
 
        self._transport.put_multi(ctrl_files)
421
 
 
422
363
    def _make_control(self):
423
 
        from bzrlib.inventory import Inventory
424
 
        from bzrlib.weavefile import write_weave_v5
425
 
        from bzrlib.weave import Weave
426
 
        
427
 
        # Create an empty inventory
428
 
        sio = StringIO()
 
364
        os.mkdir(self.controlfilename([]))
 
365
        self.controlfile('README', 'w').write(
 
366
            "This is a Bazaar-NG control directory.\n"
 
367
            "Do not change any files in this directory.\n")
 
368
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
 
369
        for d in ('text-store', 'revision-store',
 
370
                  'weaves'):
 
371
            os.mkdir(self.controlfilename(d))
 
372
        for f in ('revision-history',
 
373
                  'branch-name',
 
374
                  'branch-lock',
 
375
                  'pending-merges'):
 
376
            self.controlfile(f, 'w').write('')
 
377
        mutter('created control directory in ' + self.base)
 
378
 
429
379
        # if we want per-tree root ids then this is the place to set
430
380
        # them; they're not needed for now and so ommitted for
431
381
        # simplicity.
432
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
433
 
        empty_inv = sio.getvalue()
434
 
        sio = StringIO()
435
 
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
436
 
        empty_weave = sio.getvalue()
 
382
        f = self.controlfile('inventory','w')
 
383
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
437
384
 
438
 
        dirs = [[], 'revision-store', 'weaves']
439
 
        files = [('README', 
440
 
            "This is a Bazaar-NG control directory.\n"
441
 
            "Do not change any files in this directory.\n"),
442
 
            ('branch-format', BZR_BRANCH_FORMAT_5),
443
 
            ('revision-history', ''),
444
 
            ('branch-name', ''),
445
 
            ('branch-lock', ''),
446
 
            ('pending-merges', ''),
447
 
            ('inventory', empty_inv),
448
 
            ('inventory.weave', empty_weave),
449
 
            ('ancestry.weave', empty_weave)
450
 
        ]
451
 
        cfn = self._rel_controlfilename
452
 
        self._transport.mkdir_multi([cfn(d) for d in dirs])
453
 
        self.put_controlfiles(files)
454
 
        mutter('created control directory in ' + self._transport.base)
455
385
 
456
386
    def _check_format(self, relax_version_check):
457
387
        """Check this branch format is supported.
464
394
        """
465
395
        try:
466
396
            fmt = self.controlfile('branch-format', 'r').read()
467
 
        except NoSuchFile:
468
 
            raise NotBranchError(self.base)
469
 
        mutter("got branch format %r", fmt)
 
397
        except IOError, e:
 
398
            if e.errno == errno.ENOENT:
 
399
                raise NotBranchError(self.base)
 
400
            else:
 
401
                raise
 
402
 
470
403
        if fmt == BZR_BRANCH_FORMAT_5:
471
404
            self._branch_format = 5
472
405
        elif fmt == BZR_BRANCH_FORMAT_4:
476
409
            and self._branch_format != 5):
477
410
            raise BzrError('sorry, branch format %r not supported' % fmt,
478
411
                           ['use a different bzr version',
479
 
                            'or remove the .bzr directory'
480
 
                            ' and "bzr init" again'])
 
412
                            'or remove the .bzr directory and "bzr init" again'])
481
413
 
482
414
    def get_root_id(self):
483
415
        """Return the id of this branches root"""
514
446
        That is to say, the inventory describing changes underway, that
515
447
        will be committed to the next revision.
516
448
        """
517
 
        from cStringIO import StringIO
 
449
        from bzrlib.atomicfile import AtomicFile
 
450
        
518
451
        self.lock_write()
519
452
        try:
520
 
            sio = StringIO()
521
 
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
522
 
            sio.seek(0)
523
 
            # Transport handles atomicity
524
 
            self.put_controlfile('inventory', sio)
 
453
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
454
            try:
 
455
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
 
456
                f.commit()
 
457
            finally:
 
458
                f.close()
525
459
        finally:
526
460
            self.unlock()
527
461
        
528
462
        mutter('wrote working inventory')
529
463
            
 
464
 
530
465
    inventory = property(read_working_inventory, _write_inventory, None,
531
466
                         """Inventory for the working copy.""")
532
467
 
 
468
 
533
469
    def add(self, files, ids=None):
534
470
        """Make files versioned.
535
471
 
583
519
                    kind = file_kind(fullpath)
584
520
                except OSError:
585
521
                    # maybe something better?
586
 
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
522
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
587
523
 
588
 
                if not InventoryEntry.versionable_kind(kind):
589
 
                    raise BzrError('cannot add: not a versionable file ('
590
 
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
524
                if kind != 'file' and kind != 'directory':
 
525
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
591
526
 
592
527
                if file_id is None:
593
528
                    file_id = gen_file_id(f)
658
593
        finally:
659
594
            self.unlock()
660
595
 
 
596
 
661
597
    # FIXME: this doesn't need to be a branch method
662
598
    def set_inventory(self, new_inventory_list):
663
599
        from bzrlib.inventory import Inventory, InventoryEntry
666
602
            name = os.path.basename(path)
667
603
            if name == "":
668
604
                continue
669
 
            # fixme, there should be a factory function inv,add_?? 
670
 
            if kind == 'directory':
671
 
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
672
 
            elif kind == 'file':
673
 
                inv.add(inventory.InventoryFile(file_id, name, parent))
674
 
            elif kind == 'symlink':
675
 
                inv.add(inventory.InventoryLink(file_id, name, parent))
676
 
            else:
677
 
                raise BzrError("unknown kind %r" % kind)
 
605
            inv.add(InventoryEntry(file_id, name, kind, parent))
678
606
        self._write_inventory(inv)
679
607
 
 
608
 
680
609
    def unknowns(self):
681
610
        """Return all unknown files.
682
611
 
697
626
 
698
627
 
699
628
    def append_revision(self, *revision_ids):
 
629
        from bzrlib.atomicfile import AtomicFile
 
630
 
700
631
        for revision_id in revision_ids:
701
632
            mutter("add {%s} to revision-history" % revision_id)
702
 
        self.lock_write()
 
633
 
 
634
        rev_history = self.revision_history()
 
635
        rev_history.extend(revision_ids)
 
636
 
 
637
        f = AtomicFile(self.controlfilename('revision-history'))
703
638
        try:
704
 
            rev_history = self.revision_history()
705
 
            rev_history.extend(revision_ids)
706
 
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
639
            for rev_id in rev_history:
 
640
                print >>f, rev_id
 
641
            f.commit()
707
642
        finally:
708
 
            self.unlock()
 
643
            f.close()
 
644
 
709
645
 
710
646
    def has_revision(self, revision_id):
711
647
        """True if this branch has a copy of the revision.
715
651
        return (revision_id is None
716
652
                or revision_id in self.revision_store)
717
653
 
 
654
 
718
655
    def get_revision_xml_file(self, revision_id):
719
656
        """Return XML file object for revision object."""
720
657
        if not revision_id or not isinstance(revision_id, basestring):
729
666
        finally:
730
667
            self.unlock()
731
668
 
732
 
    #deprecated
733
 
    get_revision_xml = get_revision_xml_file
734
669
 
735
670
    def get_revision_xml(self, revision_id):
736
671
        return self.get_revision_xml_file(revision_id).read()
750
685
        assert r.revision_id == revision_id
751
686
        return r
752
687
 
 
688
 
753
689
    def get_revision_delta(self, revno):
754
690
        """Return the delta for one revision.
755
691
 
771
707
 
772
708
        return compare_trees(old_tree, new_tree)
773
709
 
 
710
 
774
711
    def get_revision_sha1(self, revision_id):
775
712
        """Hash the stored value of a revision, and return it."""
776
 
        # In the future, revision entries will be signed. At that
777
 
        # point, it is probably best *not* to include the signature
778
 
        # in the revision hash. Because that lets you re-sign
779
 
        # the revision, (add signatures/remove signatures) and still
780
 
        # have all hash pointers stay consistent.
781
 
        # But for now, just hash the contents.
782
713
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
783
714
 
 
715
 
 
716
    def _get_ancestry_weave(self):
 
717
        return self.control_weaves.get_weave('ancestry')
 
718
        
 
719
 
784
720
    def get_ancestry(self, revision_id):
785
721
        """Return a list of revision-ids integrated by a revision.
786
 
        
787
 
        This currently returns a list, but the ordering is not guaranteed:
788
 
        treat it as a set.
789
722
        """
 
723
        # strip newlines
790
724
        if revision_id is None:
791
725
            return [None]
792
 
        w = self.control_weaves.get_weave('inventory')
793
 
        return [None] + map(w.idx_to_name,
794
 
                            w.inclusions([w.lookup(revision_id)]))
 
726
        w = self._get_ancestry_weave()
 
727
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
728
 
795
729
 
796
730
    def get_inventory_weave(self):
797
731
        return self.control_weaves.get_weave('inventory')
798
732
 
 
733
 
799
734
    def get_inventory(self, revision_id):
800
735
        """Get Inventory object by hash."""
801
736
        xml = self.get_inventory_xml(revision_id)
802
737
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
803
738
 
 
739
 
804
740
    def get_inventory_xml(self, revision_id):
805
741
        """Get inventory XML as a file object."""
806
742
        try:
810
746
        except IndexError:
811
747
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
812
748
 
 
749
 
813
750
    def get_inventory_sha1(self, revision_id):
814
751
        """Return the sha1 hash of the inventory entry
815
752
        """
816
753
        return self.get_revision(revision_id).inventory_sha1
817
754
 
 
755
 
818
756
    def get_revision_inventory(self, revision_id):
819
757
        """Return inventory of a past revision."""
820
758
        # TODO: Unify this with get_inventory()
825
763
        else:
826
764
            return self.get_inventory(revision_id)
827
765
 
 
766
 
828
767
    def revision_history(self):
829
768
        """Return sequence of revision hashes on to this branch."""
830
769
        self.lock_read()
834
773
        finally:
835
774
            self.unlock()
836
775
 
 
776
 
837
777
    def common_ancestor(self, other, self_revno=None, other_revno=None):
838
778
        """
839
779
        >>> from bzrlib.commit import commit
1004
944
    def working_tree(self):
1005
945
        """Return a `Tree` for the working copy."""
1006
946
        from bzrlib.workingtree import WorkingTree
1007
 
        # TODO: In the future, WorkingTree should utilize Transport
1008
 
        # RobertCollins 20051003 - I don't think it should - working trees are
1009
 
        # much more complex to keep consistent than our careful .bzr subset.
1010
 
        # instead, we should say that working trees are local only, and optimise
1011
 
        # for that.
1012
 
        return WorkingTree(self._transport.base, self.read_working_inventory())
 
947
        return WorkingTree(self.base, self.read_working_inventory())
1013
948
 
1014
949
 
1015
950
    def basis_tree(self):
1189
1124
        These are revisions that have been merged into the working
1190
1125
        directory but not yet committed.
1191
1126
        """
1192
 
        cfn = self._rel_controlfilename('pending-merges')
1193
 
        if not self._transport.has(cfn):
 
1127
        cfn = self.controlfilename('pending-merges')
 
1128
        if not os.path.exists(cfn):
1194
1129
            return []
1195
1130
        p = []
1196
1131
        for l in self.controlfile('pending-merges', 'r').readlines():
1198
1133
        return p
1199
1134
 
1200
1135
 
1201
 
    def add_pending_merge(self, *revision_ids):
 
1136
    def add_pending_merge(self, revision_id):
 
1137
        validate_revision_id(revision_id)
1202
1138
        # TODO: Perhaps should check at this point that the
1203
1139
        # history of the revision is actually present?
1204
1140
        p = self.pending_merges()
1205
 
        updated = False
1206
 
        for rev_id in revision_ids:
1207
 
            if rev_id in p:
1208
 
                continue
1209
 
            p.append(rev_id)
1210
 
            updated = True
1211
 
        if updated:
1212
 
            self.set_pending_merges(p)
 
1141
        if revision_id in p:
 
1142
            return
 
1143
        p.append(revision_id)
 
1144
        self.set_pending_merges(p)
 
1145
 
1213
1146
 
1214
1147
    def set_pending_merges(self, rev_list):
 
1148
        from bzrlib.atomicfile import AtomicFile
1215
1149
        self.lock_write()
1216
1150
        try:
1217
 
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1151
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1152
            try:
 
1153
                for l in rev_list:
 
1154
                    print >>f, l
 
1155
                f.commit()
 
1156
            finally:
 
1157
                f.close()
1218
1158
        finally:
1219
1159
            self.unlock()
1220
1160
 
1271
1211
        
1272
1212
 
1273
1213
 
1274
 
class ScratchBranch(_Branch):
 
1214
class ScratchBranch(LocalBranch):
1275
1215
    """Special test class: a branch that cleans up after itself.
1276
1216
 
1277
1217
    >>> b = ScratchBranch()
1294
1234
        if base is None:
1295
1235
            base = mkdtemp()
1296
1236
            init = True
1297
 
        if isinstance(base, basestring):
1298
 
            base = get_transport(base)
1299
 
        _Branch.__init__(self, base, init=init)
 
1237
        LocalBranch.__init__(self, base, init=init)
1300
1238
        for d in dirs:
1301
 
            self._transport.mkdir(d)
 
1239
            os.mkdir(self.abspath(d))
1302
1240
            
1303
1241
        for f in files:
1304
 
            self._transport.put(f, 'content of %s' % f)
 
1242
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1305
1243
 
1306
1244
 
1307
1245
    def clone(self):
1324
1262
        copytree(self.base, base, symlinks=True)
1325
1263
        return ScratchBranch(base=base)
1326
1264
 
 
1265
 
 
1266
        
1327
1267
    def __del__(self):
1328
1268
        self.destroy()
1329
1269
 
1342
1282
                for name in files:
1343
1283
                    os.chmod(os.path.join(root, name), 0700)
1344
1284
            rmtree(self.base)
1345
 
        self._transport = None
 
1285
        self.base = None
1346
1286
 
1347
1287
    
1348
1288
 
1399
1339
    return gen_file_id('TREE_ROOT')
1400
1340
 
1401
1341
 
 
1342
def copy_branch(branch_from, to_location, revision=None, basis_branch=None):
 
1343
    """Copy branch_from into the existing directory to_location.
 
1344
 
 
1345
    revision
 
1346
        If not None, only revisions up to this point will be copied.
 
1347
        The head of the new branch will be that revision.  Must be a
 
1348
        revid or None.
 
1349
 
 
1350
    to_location
 
1351
        The name of a local directory that exists but is empty.
 
1352
 
 
1353
    revno
 
1354
        The revision to copy up to
 
1355
 
 
1356
    basis_branch
 
1357
        A local branch to copy revisions from, related to branch_from
 
1358
    """
 
1359
    # TODO: This could be done *much* more efficiently by just copying
 
1360
    # all the whole weaves and revisions, rather than getting one
 
1361
    # revision at a time.
 
1362
    from bzrlib.merge import merge
 
1363
 
 
1364
    assert isinstance(branch_from, Branch)
 
1365
    assert isinstance(to_location, basestring)
 
1366
    
 
1367
    br_to = Branch.initialize(to_location)
 
1368
    mutter("copy branch from %s to %s", branch_from, br_to)
 
1369
    if basis_branch is not None:
 
1370
        basis_branch.push_stores(br_to)
 
1371
    br_to.set_root_id(branch_from.get_root_id())
 
1372
    if revision is None:
 
1373
        revision = branch_from.last_revision()
 
1374
    br_to.update_revisions(branch_from, stop_revision=revision)
 
1375
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1376
          check_clean=False, ignore_zero=True)
 
1377
    br_to.set_parent(branch_from.base)
 
1378
    mutter("copied")
 
1379
    return br_to