~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: John Arbash Meinel
  • Date: 2005-09-29 20:34:25 UTC
  • mfrom: (1185.11.24)
  • mto: (1393.1.12)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050929203425-7fc2ea87f449dfe8
Merged in split-storage-2 branch. Need to cleanup a little bit more still.

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
39
from bzrlib.weavestore import WeaveStore
39
 
from bzrlib.store import copy_all, ImmutableStore
 
40
from bzrlib.store import copy_all
 
41
from bzrlib.store.compressed_text import CompressedTextStore
 
42
from bzrlib.transport import Transport
40
43
import bzrlib.xml5
41
44
import bzrlib.ui
42
45
 
55
58
    # XXX: leave this here for about one release, then remove it
56
59
    raise NotImplementedError('find_branch() is not supported anymore, '
57
60
                              'please use one of the new branch constructors')
58
 
 
59
61
def _relpath(base, path):
60
62
    """Return path relative to base, or raise exception.
61
63
 
83
85
    return os.sep.join(s)
84
86
        
85
87
 
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.
 
88
def find_branch_root(t):
 
89
    """Find the branch root enclosing the transport's base.
 
90
 
 
91
    t is a Transport object.
 
92
 
 
93
    It is not necessary that the base of t exists.
92
94
 
93
95
    Basically we keep looking up until we find the control directory or
94
96
    run into the root.  If there isn't one, raises NotBranchError.
95
97
    """
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
 
 
 
98
    orig_base = t.base
108
99
    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:
 
100
        if t.has(bzrlib.BZRDIR):
 
101
            return t
 
102
        new_t = t.clone('..')
 
103
        if new_t.base == t.base:
113
104
            # reached the root, whatever that may be
114
 
            raise NotBranchError('%s is not in a branch' % orig_f)
115
 
        f = head
116
 
 
117
 
 
 
105
            raise NotBranchError('%s is not in a branch' % orig_base)
 
106
        t = new_t
118
107
 
119
108
 
120
109
######################################################################
136
125
        """Open a branch which may be of an old format.
137
126
        
138
127
        Only local branches are supported."""
139
 
        return LocalBranch(base, find_root=False, relax_version_check=True)
 
128
        return _Branch(base, relax_version_check=True)
140
129
        
141
130
    @staticmethod
142
131
    def open(base):
143
132
        """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)
 
133
        t = bzrlib.transport.transport(base)
 
134
        return _Branch(t)
149
135
 
150
136
    @staticmethod
151
137
    def open_containing(url):
153
139
        
154
140
        This probes for a branch at url, and searches upwards from there.
155
141
        """
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)
 
142
        t = bzrlib.transport.transport(url)
 
143
        t = find_branch_root(t)
 
144
        return _Branch(t)
161
145
 
162
146
    @staticmethod
163
147
    def initialize(base):
164
148
        """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)
 
149
        t = bzrlib.transport.transport(base)
 
150
        return _Branch(t, init=True)
170
151
 
171
152
    def setup_caching(self, cache_root):
172
153
        """Subclasses that care about caching should override this, and set
174
155
        """
175
156
 
176
157
 
177
 
class LocalBranch(Branch):
 
158
class _Branch(Branch):
178
159
    """A branch stored in the actual filesystem.
179
160
 
180
161
    Note that it's "local" in the context of the filesystem; it doesn't
225
206
        except UnlistableStore:
226
207
            raise UnlistableBranch(from_store)
227
208
 
228
 
    def __init__(self, base, init=False, find_root=True,
 
209
    def __init__(self, transport, init=False,
229
210
                 relax_version_check=False):
230
211
        """Create new branch object at a particular location.
231
212
 
232
 
        base -- Base directory for the branch. May be a file:// url.
 
213
        transport -- A Transport object, defining how to access files.
 
214
                (If a string, transport.transport() will be used to
 
215
                create a Transport object)
233
216
        
234
217
        init -- If True, create new control files in a previously
235
218
             unversioned directory.  If False, the branch must already
236
219
             be versioned.
237
220
 
238
 
        find_root -- If true and init is false, find the root of the
239
 
             existing branch containing base.
240
 
 
241
221
        relax_version_check -- If true, the usual check for the branch
242
222
            version is not applied.  This is intended only for
243
223
            upgrade/recovery type use; it's not guaranteed that
246
226
        In the test suite, creation of new trees is tested using the
247
227
        `ScratchBranch` class.
248
228
        """
 
229
        assert isinstance(transport, Transport)
 
230
        self._transport = transport
249
231
        if init:
250
 
            self.base = os.path.realpath(base)
251
232
            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
233
        self._check_format(relax_version_check)
263
234
        cfn = self.controlfilename
264
235
        if self._branch_format == 4:
265
 
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
266
 
            self.text_store = ImmutableStore(cfn('text-store'))
 
236
            self.inventory_store = CompressedTextStore(cfn('inventory-store'))
 
237
            self.text_store = CompressedTextStore(cfn('text-store'))
267
238
        elif self._branch_format == 5:
268
239
            self.control_weaves = WeaveStore(cfn([]))
269
240
            self.weave_store = WeaveStore(cfn('weaves'))
271
242
                # FIXME: Unify with make_control_files
272
243
                self.control_weaves.put_empty_weave('inventory')
273
244
                self.control_weaves.put_empty_weave('ancestry')
274
 
        self.revision_store = ImmutableStore(cfn('revision-store'))
275
 
 
 
245
        self.revision_store = CompressedTextStore(cfn('revision-store'))
276
246
 
277
247
    def __str__(self):
278
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
248
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
279
249
 
280
250
 
281
251
    __repr__ = __str__
288
258
            warn("branch %r was not explicitly unlocked" % self)
289
259
            self._lock.unlock()
290
260
 
 
261
        # TODO: It might be best to do this somewhere else,
 
262
        # but it is nice for a Branch object to automatically
 
263
        # cache it's information.
 
264
        # Alternatively, we could have the Transport objects cache requests
 
265
        # See the earlier discussion about how major objects (like Branch)
 
266
        # should never expect their __del__ function to run.
 
267
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
268
            try:
 
269
                import shutil
 
270
                shutil.rmtree(self.cache_root)
 
271
            except:
 
272
                pass
 
273
            self.cache_root = None
 
274
 
 
275
    def _get_base(self):
 
276
        if self._transport:
 
277
            return self._transport.base
 
278
        return None
 
279
 
 
280
    base = property(_get_base)
 
281
 
 
282
 
291
283
    def lock_write(self):
 
284
        # TODO: Upgrade locking to support using a Transport,
 
285
        # and potentially a remote locking protocol
292
286
        if self._lock_mode:
293
287
            if self._lock_mode != 'w':
294
288
                raise LockError("can't upgrade to a write lock from %r" %
295
289
                                self._lock_mode)
296
290
            self._lock_count += 1
297
291
        else:
298
 
            from bzrlib.lock import WriteLock
299
 
 
300
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
292
            self._lock = self._transport.lock_write(
 
293
                    self._rel_controlfilename('branch-lock'))
301
294
            self._lock_mode = 'w'
302
295
            self._lock_count = 1
303
296
 
308
301
                   "invalid lock mode %r" % self._lock_mode
309
302
            self._lock_count += 1
310
303
        else:
311
 
            from bzrlib.lock import ReadLock
312
 
 
313
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
304
            self._lock = self._transport.lock_read(
 
305
                    self._rel_controlfilename('branch-lock'))
314
306
            self._lock_mode = 'r'
315
307
            self._lock_count = 1
316
308
                        
327
319
 
328
320
    def abspath(self, name):
329
321
        """Return absolute filename for something in the branch"""
330
 
        return os.path.join(self.base, name)
 
322
        return self._transport.abspath(name)
331
323
 
332
324
    def relpath(self, path):
333
325
        """Return path relative to this branch of something inside it.
334
326
 
335
327
        Raises an error if path is not in this branch."""
336
 
        return _relpath(self.base, path)
 
328
        return self._transport.relpath(path)
 
329
 
 
330
 
 
331
    def _rel_controlfilename(self, file_or_path):
 
332
        if isinstance(file_or_path, basestring):
 
333
            file_or_path = [file_or_path]
 
334
        return [bzrlib.BZRDIR] + file_or_path
337
335
 
338
336
    def controlfilename(self, file_or_path):
339
337
        """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)
 
338
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
343
339
 
344
340
 
345
341
    def controlfile(self, file_or_path, mode='r'):
353
349
        Controlfiles should almost never be opened in write mode but
354
350
        rather should be atomically copied and replaced using atomicfile.
355
351
        """
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)
 
352
        import codecs
 
353
 
 
354
        relpath = self._rel_controlfilename(file_or_path)
 
355
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
356
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
357
        if mode == 'rb': 
 
358
            return self._transport.get(relpath)
 
359
        elif mode == 'wb':
 
360
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
361
        elif mode == 'r':
 
362
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
363
        elif mode == 'w':
 
364
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
367
365
        else:
368
366
            raise BzrError("invalid controlfile mode %r" % mode)
369
367
 
 
368
    def put_controlfile(self, path, f, encode=True):
 
369
        """Write an entry as a controlfile.
 
370
 
 
371
        :param path: The path to put the file, relative to the .bzr control
 
372
                     directory
 
373
        :param f: A file-like or string object whose contents should be copied.
 
374
        :param encode:  If true, encode the contents as utf-8
 
375
        """
 
376
        self.put_controlfiles([(path, f)], encode=encode)
 
377
 
 
378
    def put_controlfiles(self, files, encode=True):
 
379
        """Write several entries as controlfiles.
 
380
 
 
381
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
382
                      underneath the bzr control directory
 
383
        :param encode:  If true, encode the contents as utf-8
 
384
        """
 
385
        import codecs
 
386
        ctrl_files = []
 
387
        for path, f in files:
 
388
            if encode:
 
389
                if isinstance(f, basestring):
 
390
                    f = f.encode('utf-8', 'replace')
 
391
                else:
 
392
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
393
            path = self._rel_controlfilename(path)
 
394
            ctrl_files.append((path, f))
 
395
        self._transport.put_multi(ctrl_files)
 
396
 
370
397
    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
 
 
 
398
        from bzrlib.inventory import Inventory
 
399
        
 
400
        # Create an empty inventory
 
401
        sio = StringIO()
386
402
        # if we want per-tree root ids then this is the place to set
387
403
        # them; they're not needed for now and so ommitted for
388
404
        # simplicity.
389
 
        f = self.controlfile('inventory','w')
390
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
405
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
391
406
 
 
407
        dirs = [[], 'revision-store', 'weaves']
 
408
        files = [('README', 
 
409
            "This is a Bazaar-NG control directory.\n"
 
410
            "Do not change any files in this directory.\n"),
 
411
            ('branch-format', BZR_BRANCH_FORMAT_5),
 
412
            ('revision-history', ''),
 
413
            ('branch-name', ''),
 
414
            ('branch-lock', ''),
 
415
            ('pending-merges', ''),
 
416
            ('inventory', sio.getvalue())
 
417
        ]
 
418
        cfn = self._rel_controlfilename
 
419
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
420
        self.put_controlfiles(files)
 
421
        mutter('created control directory in ' + self._transport.base)
392
422
 
393
423
    def _check_format(self, relax_version_check):
394
424
        """Check this branch format is supported.
401
431
        """
402
432
        try:
403
433
            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
 
434
        except NoSuchFile:
 
435
            raise NotBranchError(self.base)
409
436
 
410
437
        if fmt == BZR_BRANCH_FORMAT_5:
411
438
            self._branch_format = 5
416
443
            and self._branch_format != 5):
417
444
            raise BzrError('sorry, branch format %r not supported' % fmt,
418
445
                           ['use a different bzr version',
419
 
                            'or remove the .bzr directory and "bzr init" again'])
 
446
                            'or remove the .bzr directory'
 
447
                            ' and "bzr init" again'])
420
448
 
421
449
    def get_root_id(self):
422
450
        """Return the id of this branches root"""
453
481
        That is to say, the inventory describing changes underway, that
454
482
        will be committed to the next revision.
455
483
        """
456
 
        from bzrlib.atomicfile import AtomicFile
457
 
        
 
484
        from cStringIO import StringIO
458
485
        self.lock_write()
459
486
        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()
 
487
            sio = StringIO()
 
488
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
489
            sio.seek(0)
 
490
            # Transport handles atomicity
 
491
            self.put_controlfile('inventory', sio)
466
492
        finally:
467
493
            self.unlock()
468
494
        
633
659
 
634
660
 
635
661
    def append_revision(self, *revision_ids):
636
 
        from bzrlib.atomicfile import AtomicFile
637
 
 
638
662
        for revision_id in revision_ids:
639
663
            mutter("add {%s} to revision-history" % revision_id)
640
664
 
641
665
        rev_history = self.revision_history()
642
666
        rev_history.extend(revision_ids)
643
667
 
644
 
        f = AtomicFile(self.controlfilename('revision-history'))
 
668
        self.lock_write()
645
669
        try:
646
 
            for rev_id in rev_history:
647
 
                print >>f, rev_id
648
 
            f.commit()
 
670
            self.put_controlfile('revision-history', '\n'.join(rev_history))
649
671
        finally:
650
 
            f.close()
 
672
            self.unlock()
651
673
 
652
674
 
653
675
    def has_revision(self, revision_id):
714
736
 
715
737
        return compare_trees(old_tree, new_tree)
716
738
 
717
 
 
718
739
    def get_revision_sha1(self, revision_id):
719
740
        """Hash the stored value of a revision, and return it."""
720
741
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
951
972
    def working_tree(self):
952
973
        """Return a `Tree` for the working copy."""
953
974
        from bzrlib.workingtree import WorkingTree
954
 
        return WorkingTree(self.base, self.read_working_inventory())
 
975
        # TODO: In the future, WorkingTree should utilize Transport
 
976
        return WorkingTree(self._transport.base, self.read_working_inventory())
955
977
 
956
978
 
957
979
    def basis_tree(self):
1131
1153
        These are revisions that have been merged into the working
1132
1154
        directory but not yet committed.
1133
1155
        """
1134
 
        cfn = self.controlfilename('pending-merges')
1135
 
        if not os.path.exists(cfn):
 
1156
        cfn = self._rel_controlfilename('pending-merges')
 
1157
        if not self._transport.has(cfn):
1136
1158
            return []
1137
1159
        p = []
1138
1160
        for l in self.controlfile('pending-merges', 'r').readlines():
1140
1162
        return p
1141
1163
 
1142
1164
 
1143
 
    def add_pending_merge(self, revision_id):
1144
 
        validate_revision_id(revision_id)
 
1165
    def add_pending_merge(self, *revision_ids):
1145
1166
        # TODO: Perhaps should check at this point that the
1146
1167
        # history of the revision is actually present?
 
1168
        for rev_id in revision_ids:
 
1169
            validate_revision_id(rev_id)
 
1170
 
1147
1171
        p = self.pending_merges()
1148
 
        if revision_id in p:
1149
 
            return
1150
 
        p.append(revision_id)
1151
 
        self.set_pending_merges(p)
1152
 
 
 
1172
        updated = False
 
1173
        for rev_id in revision_ids:
 
1174
            if rev_id in p:
 
1175
                continue
 
1176
            p.append(rev_id)
 
1177
            updated = True
 
1178
        if updated:
 
1179
            self.set_pending_merges(p)
1153
1180
 
1154
1181
    def set_pending_merges(self, rev_list):
1155
 
        from bzrlib.atomicfile import AtomicFile
1156
1182
        self.lock_write()
1157
1183
        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()
 
1184
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1165
1185
        finally:
1166
1186
            self.unlock()
1167
1187
 
1218
1238
        
1219
1239
 
1220
1240
 
1221
 
class ScratchBranch(LocalBranch):
 
1241
class ScratchBranch(_Branch):
1222
1242
    """Special test class: a branch that cleans up after itself.
1223
1243
 
1224
1244
    >>> b = ScratchBranch()
1241
1261
        if base is None:
1242
1262
            base = mkdtemp()
1243
1263
            init = True
1244
 
        LocalBranch.__init__(self, base, init=init)
 
1264
        _Branch.__init__(self, base, init=init)
1245
1265
        for d in dirs:
1246
 
            os.mkdir(self.abspath(d))
 
1266
            self._transport.mkdir(d)
1247
1267
            
1248
1268
        for f in files:
1249
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1269
            self._transport.put(f, 'content of %s' % f)
1250
1270
 
1251
1271
 
1252
1272
    def clone(self):
1289
1309
                for name in files:
1290
1310
                    os.chmod(os.path.join(root, name), 0700)
1291
1311
            rmtree(self.base)
1292
 
        self.base = None
 
1312
        self._transport = None
1293
1313
 
1294
1314
    
1295
1315