~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-06 00:38:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050706003813-3994f3d9e806a259
- better error when failing to run selftest on python2.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
 
import traceback, socket, fnmatch, difflib, time
20
 
from binascii import hexlify
 
18
import sys, os
21
19
 
22
20
import bzrlib
23
 
from inventory import Inventory
24
 
from trace import mutter, note
25
 
from tree import Tree, EmptyTree, RevisionTree
26
 
from inventory import InventoryEntry, Inventory
27
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
 
     joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath
30
 
from store import ImmutableStore
31
 
from revision import Revision
32
 
from errors import BzrError
33
 
from textui import show_status
 
21
from bzrlib.trace import mutter, note
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
     sha_file, appendpath, file_kind
 
24
from bzrlib.errors import BzrError
34
25
 
35
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
27
## TODO: Maybe include checks for common corruption of newlines, etc?
45
36
        return Branch(f, **args)
46
37
 
47
38
 
 
39
def find_cached_branch(f, cache_root, **args):
 
40
    from remotebranch import RemoteBranch
 
41
    br = find_branch(f, **args)
 
42
    def cacheify(br, store_name):
 
43
        from meta_store import CachedStore
 
44
        cache_path = os.path.join(cache_root, store_name)
 
45
        os.mkdir(cache_path)
 
46
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
47
        setattr(br, store_name, new_store)
 
48
 
 
49
    if isinstance(br, RemoteBranch):
 
50
        cacheify(br, 'inventory_store')
 
51
        cacheify(br, 'text_store')
 
52
        cacheify(br, 'revision_store')
 
53
    return br
 
54
 
48
55
 
49
56
def _relpath(base, path):
50
57
    """Return path relative to base, or raise exception.
158
165
        In the test suite, creation of new trees is tested using the
159
166
        `ScratchBranch` class.
160
167
        """
 
168
        from bzrlib.store import ImmutableStore
161
169
        if init:
162
170
            self.base = os.path.realpath(base)
163
171
            self._make_control()
249
257
 
250
258
    def controlfilename(self, file_or_path):
251
259
        """Return location relative to branch."""
252
 
        if isinstance(file_or_path, types.StringTypes):
 
260
        if isinstance(file_or_path, basestring):
253
261
            file_or_path = [file_or_path]
254
262
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
255
263
 
282
290
 
283
291
 
284
292
    def _make_control(self):
 
293
        from bzrlib.inventory import Inventory
 
294
        from bzrlib.xml import pack_xml
 
295
        
285
296
        os.mkdir(self.controlfilename([]))
286
297
        self.controlfile('README', 'w').write(
287
298
            "This is a Bazaar-NG control directory.\n"
291
302
            os.mkdir(self.controlfilename(d))
292
303
        for f in ('revision-history', 'merged-patches',
293
304
                  'pending-merged-patches', 'branch-name',
294
 
                  'branch-lock'):
 
305
                  'branch-lock',
 
306
                  'pending-merges'):
295
307
            self.controlfile(f, 'w').write('')
296
308
        mutter('created control directory in ' + self.base)
297
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
309
 
 
310
        pack_xml(Inventory(), self.controlfile('inventory','w'))
298
311
 
299
312
 
300
313
    def _check_format(self):
319
332
 
320
333
    def read_working_inventory(self):
321
334
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
 
335
        from bzrlib.inventory import Inventory
 
336
        from bzrlib.xml import unpack_xml
 
337
        from time import time
 
338
        before = time()
325
339
        self.lock_read()
326
340
        try:
327
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
341
            # ElementTree does its own conversion from UTF-8, so open in
 
342
            # binary.
 
343
            inv = unpack_xml(Inventory,
 
344
                                  self.controlfile('inventory', 'rb'))
328
345
            mutter("loaded inventory of %d items in %f"
329
 
                   % (len(inv), time.time() - before))
 
346
                   % (len(inv), time() - before))
330
347
            return inv
331
348
        finally:
332
349
            self.unlock()
338
355
        That is to say, the inventory describing changes underway, that
339
356
        will be committed to the next revision.
340
357
        """
341
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
342
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
343
 
        tmpfname = self.controlfilename('inventory.tmp')
344
 
        tmpf = file(tmpfname, 'wb')
345
 
        inv.write_xml(tmpf)
346
 
        tmpf.close()
347
 
        inv_fname = self.controlfilename('inventory')
348
 
        if sys.platform == 'win32':
349
 
            os.remove(inv_fname)
350
 
        os.rename(tmpfname, inv_fname)
 
358
        from bzrlib.atomicfile import AtomicFile
 
359
        from bzrlib.xml import pack_xml
 
360
        
 
361
        self.lock_write()
 
362
        try:
 
363
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
364
            try:
 
365
                pack_xml(inv, f)
 
366
                f.commit()
 
367
            finally:
 
368
                f.close()
 
369
        finally:
 
370
            self.unlock()
 
371
        
351
372
        mutter('wrote working inventory')
352
373
            
353
374
 
381
402
              add all non-ignored children.  Perhaps do that in a
382
403
              higher-level method.
383
404
        """
 
405
        from bzrlib.textui import show_status
384
406
        # TODO: Re-adding a file that is removed in the working copy
385
407
        # should probably put it back with the previous ID.
386
 
        if isinstance(files, types.StringTypes):
387
 
            assert(ids is None or isinstance(ids, types.StringTypes))
 
408
        if isinstance(files, basestring):
 
409
            assert(ids is None or isinstance(ids, basestring))
388
410
            files = [files]
389
411
            if ids is not None:
390
412
                ids = [ids]
422
444
                inv.add_path(f, kind=kind, file_id=file_id)
423
445
 
424
446
                if verbose:
425
 
                    show_status('A', kind, quotefn(f))
 
447
                    print 'added', quotefn(f)
426
448
 
427
449
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
428
450
 
459
481
        is the opposite of add.  Removing it is consistent with most
460
482
        other tools.  Maybe an option.
461
483
        """
 
484
        from bzrlib.textui import show_status
462
485
        ## TODO: Normalize names
463
486
        ## TODO: Remove nested loops; better scalability
464
 
        if isinstance(files, types.StringTypes):
 
487
        if isinstance(files, basestring):
465
488
            files = [files]
466
489
 
467
490
        self.lock_write()
492
515
 
493
516
    # FIXME: this doesn't need to be a branch method
494
517
    def set_inventory(self, new_inventory_list):
 
518
        from bzrlib.inventory import Inventory, InventoryEntry
495
519
        inv = Inventory()
496
520
        for path, file_id, parent, kind in new_inventory_list:
497
521
            name = os.path.basename(path)
521
545
 
522
546
 
523
547
    def append_revision(self, revision_id):
 
548
        from bzrlib.atomicfile import AtomicFile
 
549
 
524
550
        mutter("add {%s} to revision-history" % revision_id)
525
 
        rev_history = self.revision_history()
526
 
 
527
 
        tmprhname = self.controlfilename('revision-history.tmp')
528
 
        rhname = self.controlfilename('revision-history')
529
 
        
530
 
        f = file(tmprhname, 'wt')
531
 
        rev_history.append(revision_id)
532
 
        f.write('\n'.join(rev_history))
533
 
        f.write('\n')
534
 
        f.close()
535
 
 
536
 
        if sys.platform == 'win32':
537
 
            os.remove(rhname)
538
 
        os.rename(tmprhname, rhname)
539
 
        
 
551
        rev_history = self.revision_history() + [revision_id]
 
552
 
 
553
        f = AtomicFile(self.controlfilename('revision-history'))
 
554
        try:
 
555
            for rev_id in rev_history:
 
556
                print >>f, rev_id
 
557
            f.commit()
 
558
        finally:
 
559
            f.close()
540
560
 
541
561
 
542
562
    def get_revision(self, revision_id):
543
563
        """Return the Revision object for a named revision"""
544
 
        if not revision_id or not isinstance(revision_id, basestring):
545
 
            raise ValueError('invalid revision-id: %r' % revision_id)
546
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
564
        from bzrlib.revision import Revision
 
565
        from bzrlib.xml import unpack_xml
 
566
 
 
567
        self.lock_read()
 
568
        try:
 
569
            if not revision_id or not isinstance(revision_id, basestring):
 
570
                raise ValueError('invalid revision-id: %r' % revision_id)
 
571
            r = unpack_xml(Revision, self.revision_store[revision_id])
 
572
        finally:
 
573
            self.unlock()
 
574
            
547
575
        assert r.revision_id == revision_id
548
576
        return r
 
577
        
549
578
 
550
579
    def get_revision_sha1(self, revision_id):
551
580
        """Hash the stored value of a revision, and return it."""
564
593
        TODO: Perhaps for this and similar methods, take a revision
565
594
               parameter which can be either an integer revno or a
566
595
               string hash."""
567
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
568
 
        return i
 
596
        from bzrlib.inventory import Inventory
 
597
        from bzrlib.xml import unpack_xml
 
598
 
 
599
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
600
            
569
601
 
570
602
    def get_inventory_sha1(self, inventory_id):
571
603
        """Return the sha1 hash of the inventory entry
575
607
 
576
608
    def get_revision_inventory(self, revision_id):
577
609
        """Return inventory of a past revision."""
 
610
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
611
        # must be the same as its revision, so this is trivial.
578
612
        if revision_id == None:
 
613
            from bzrlib.inventory import Inventory
579
614
            return Inventory()
580
615
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
616
            return self.get_inventory(revision_id)
582
617
 
583
618
 
584
619
    def revision_history(self):
748
783
        True
749
784
        """
750
785
        from bzrlib.progress import ProgressBar
 
786
        try:
 
787
            set
 
788
        except NameError:
 
789
            from sets import Set as set
751
790
 
752
791
        pb = ProgressBar()
753
792
 
754
793
        pb.update('comparing histories')
755
794
        revision_ids = self.missing_revisions(other, stop_revision)
 
795
 
 
796
        if hasattr(other.revision_store, "prefetch"):
 
797
            other.revision_store.prefetch(revision_ids)
 
798
        if hasattr(other.inventory_store, "prefetch"):
 
799
            inventory_ids = [other.get_revision(r).inventory_id
 
800
                             for r in revision_ids]
 
801
            other.inventory_store.prefetch(inventory_ids)
 
802
                
756
803
        revisions = []
757
 
        needed_texts = sets.Set()
 
804
        needed_texts = set()
758
805
        i = 0
759
806
        for rev_id in revision_ids:
760
807
            i += 1
785
832
                    
786
833
        
787
834
    def commit(self, *args, **kw):
788
 
        """Deprecated"""
789
835
        from bzrlib.commit import commit
790
836
        commit(self, *args, **kw)
791
837
        
807
853
 
808
854
        `revision_id` may be None for the null revision, in which case
809
855
        an `EmptyTree` is returned."""
 
856
        from bzrlib.tree import EmptyTree, RevisionTree
810
857
        # TODO: refactor this to use an existing revision object
811
858
        # so we don't need to read it in twice.
812
859
        if revision_id == None:
827
874
 
828
875
        If there are no revisions yet, return an `EmptyTree`.
829
876
        """
 
877
        from bzrlib.tree import EmptyTree, RevisionTree
830
878
        r = self.last_patch()
831
879
        if r == None:
832
880
            return EmptyTree()
950
998
            self.unlock()
951
999
 
952
1000
 
 
1001
    def revert(self, filenames, old_tree=None, backups=True):
 
1002
        """Restore selected files to the versions from a previous tree.
 
1003
 
 
1004
        backups
 
1005
            If true (default) backups are made of files before
 
1006
            they're renamed.
 
1007
        """
 
1008
        from bzrlib.errors import NotVersionedError, BzrError
 
1009
        from bzrlib.atomicfile import AtomicFile
 
1010
        from bzrlib.osutils import backup_file
 
1011
        
 
1012
        inv = self.read_working_inventory()
 
1013
        if old_tree is None:
 
1014
            old_tree = self.basis_tree()
 
1015
        old_inv = old_tree.inventory
 
1016
 
 
1017
        nids = []
 
1018
        for fn in filenames:
 
1019
            file_id = inv.path2id(fn)
 
1020
            if not file_id:
 
1021
                raise NotVersionedError("not a versioned file", fn)
 
1022
            if not old_inv.has_id(file_id):
 
1023
                raise BzrError("file not present in old tree", fn, file_id)
 
1024
            nids.append((fn, file_id))
 
1025
            
 
1026
        # TODO: Rename back if it was previously at a different location
 
1027
 
 
1028
        # TODO: If given a directory, restore the entire contents from
 
1029
        # the previous version.
 
1030
 
 
1031
        # TODO: Make a backup to a temporary file.
 
1032
 
 
1033
        # TODO: If the file previously didn't exist, delete it?
 
1034
        for fn, file_id in nids:
 
1035
            backup_file(fn)
 
1036
            
 
1037
            f = AtomicFile(fn, 'wb')
 
1038
            try:
 
1039
                f.write(old_tree.get_file(file_id).read())
 
1040
                f.commit()
 
1041
            finally:
 
1042
                f.close()
 
1043
 
 
1044
 
 
1045
    def pending_merges(self):
 
1046
        """Return a list of pending merges.
 
1047
 
 
1048
        These are revisions that have been merged into the working
 
1049
        directory but not yet committed.
 
1050
        """
 
1051
        cfn = self.controlfilename('pending-merges')
 
1052
        if not os.path.exists(cfn):
 
1053
            return []
 
1054
        p = []
 
1055
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1056
            p.append(l.rstrip('\n'))
 
1057
        return p
 
1058
 
 
1059
 
 
1060
    def add_pending_merge(self, revision_id):
 
1061
        from bzrlib.revision import validate_revision_id
 
1062
 
 
1063
        validate_revision_id(revision_id)
 
1064
 
 
1065
        p = self.pending_merges()
 
1066
        if revision_id in p:
 
1067
            return
 
1068
        p.append(revision_id)
 
1069
        self.set_pending_merges(p)
 
1070
 
 
1071
 
 
1072
    def set_pending_merges(self, rev_list):
 
1073
        from bzrlib.atomicfile import AtomicFile
 
1074
        self.lock_write()
 
1075
        try:
 
1076
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1077
            try:
 
1078
                for l in rev_list:
 
1079
                    print >>f, l
 
1080
                f.commit()
 
1081
            finally:
 
1082
                f.close()
 
1083
        finally:
 
1084
            self.unlock()
 
1085
 
 
1086
 
953
1087
 
954
1088
class ScratchBranch(Branch):
955
1089
    """Special test class: a branch that cleans up after itself.
969
1103
 
970
1104
        If any files are listed, they are created in the working copy.
971
1105
        """
 
1106
        from tempfile import mkdtemp
972
1107
        init = False
973
1108
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1109
            base = mkdtemp()
975
1110
            init = True
976
1111
        Branch.__init__(self, base, init=init)
977
1112
        for d in dirs:
990
1125
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1126
        True
992
1127
        """
993
 
        base = tempfile.mkdtemp()
 
1128
        from shutil import copytree
 
1129
        from tempfile import mkdtemp
 
1130
        base = mkdtemp()
994
1131
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1132
        copytree(self.base, base, symlinks=True)
996
1133
        return ScratchBranch(base=base)
997
1134
        
998
1135
    def __del__(self):
1000
1137
 
1001
1138
    def destroy(self):
1002
1139
        """Destroy the test branch, removing the scratch directory."""
 
1140
        from shutil import rmtree
1003
1141
        try:
1004
1142
            if self.base:
1005
1143
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1144
                rmtree(self.base)
1007
1145
        except OSError, e:
1008
1146
            # Work around for shutil.rmtree failing on Windows when
1009
1147
            # readonly files are encountered
1011
1149
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1150
                for name in files:
1013
1151
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1152
            rmtree(self.base)
1015
1153
        self.base = None
1016
1154
 
1017
1155
    
1042
1180
    cope with just randomness because running uuidgen every time is
1043
1181
    slow."""
1044
1182
    import re
 
1183
    from binascii import hexlify
 
1184
    from time import time
1045
1185
 
1046
1186
    # get last component
1047
1187
    idx = name.rfind('/')
1059
1199
    name = re.sub(r'[^\w.]', '', name)
1060
1200
 
1061
1201
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1202
    return '-'.join((name, compact_date(time()), s))