~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-06-21 06:10:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050621061017-12e8f0ff45228338
- move whitebox/blackbox modules into bzrlib.selftest subdirectory

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
 
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
19
21
 
20
22
import bzrlib
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
 
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
25
34
 
26
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
36
## TODO: Maybe include checks for common corruption of newlines, etc?
36
45
        return Branch(f, **args)
37
46
 
38
47
 
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
 
 
55
48
 
56
49
def _relpath(base, path):
57
50
    """Return path relative to base, or raise exception.
165
158
        In the test suite, creation of new trees is tested using the
166
159
        `ScratchBranch` class.
167
160
        """
168
 
        from bzrlib.store import ImmutableStore
169
161
        if init:
170
162
            self.base = os.path.realpath(base)
171
163
            self._make_control()
257
249
 
258
250
    def controlfilename(self, file_or_path):
259
251
        """Return location relative to branch."""
260
 
        if isinstance(file_or_path, basestring):
 
252
        if isinstance(file_or_path, types.StringTypes):
261
253
            file_or_path = [file_or_path]
262
254
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
263
255
 
290
282
 
291
283
 
292
284
    def _make_control(self):
293
 
        from bzrlib.inventory import Inventory
294
 
        from bzrlib.xml import pack_xml
295
 
        
296
285
        os.mkdir(self.controlfilename([]))
297
286
        self.controlfile('README', 'w').write(
298
287
            "This is a Bazaar-NG control directory.\n"
302
291
            os.mkdir(self.controlfilename(d))
303
292
        for f in ('revision-history', 'merged-patches',
304
293
                  'pending-merged-patches', 'branch-name',
305
 
                  'branch-lock',
306
 
                  'pending-merges'):
 
294
                  'branch-lock'):
307
295
            self.controlfile(f, 'w').write('')
308
296
        mutter('created control directory in ' + self.base)
309
 
 
310
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
297
        Inventory().write_xml(self.controlfile('inventory','w'))
311
298
 
312
299
 
313
300
    def _check_format(self):
332
319
 
333
320
    def read_working_inventory(self):
334
321
        """Read the working inventory."""
335
 
        from bzrlib.inventory import Inventory
336
 
        from bzrlib.xml import unpack_xml
337
 
        from time import time
338
 
        before = time()
 
322
        before = time.time()
 
323
        # ElementTree does its own conversion from UTF-8, so open in
 
324
        # binary.
339
325
        self.lock_read()
340
326
        try:
341
 
            # ElementTree does its own conversion from UTF-8, so open in
342
 
            # binary.
343
 
            inv = unpack_xml(Inventory,
344
 
                                  self.controlfile('inventory', 'rb'))
 
327
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
345
328
            mutter("loaded inventory of %d items in %f"
346
 
                   % (len(inv), time() - before))
 
329
                   % (len(inv), time.time() - before))
347
330
            return inv
348
331
        finally:
349
332
            self.unlock()
355
338
        That is to say, the inventory describing changes underway, that
356
339
        will be committed to the next revision.
357
340
        """
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
 
        
 
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)
372
351
        mutter('wrote working inventory')
373
352
            
374
353
 
402
381
              add all non-ignored children.  Perhaps do that in a
403
382
              higher-level method.
404
383
        """
405
 
        from bzrlib.textui import show_status
406
384
        # TODO: Re-adding a file that is removed in the working copy
407
385
        # should probably put it back with the previous ID.
408
 
        if isinstance(files, basestring):
409
 
            assert(ids is None or isinstance(ids, basestring))
 
386
        if isinstance(files, types.StringTypes):
 
387
            assert(ids is None or isinstance(ids, types.StringTypes))
410
388
            files = [files]
411
389
            if ids is not None:
412
390
                ids = [ids]
444
422
                inv.add_path(f, kind=kind, file_id=file_id)
445
423
 
446
424
                if verbose:
447
 
                    print 'added', quotefn(f)
 
425
                    show_status('A', kind, quotefn(f))
448
426
 
449
427
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
450
428
 
481
459
        is the opposite of add.  Removing it is consistent with most
482
460
        other tools.  Maybe an option.
483
461
        """
484
 
        from bzrlib.textui import show_status
485
462
        ## TODO: Normalize names
486
463
        ## TODO: Remove nested loops; better scalability
487
 
        if isinstance(files, basestring):
 
464
        if isinstance(files, types.StringTypes):
488
465
            files = [files]
489
466
 
490
467
        self.lock_write()
515
492
 
516
493
    # FIXME: this doesn't need to be a branch method
517
494
    def set_inventory(self, new_inventory_list):
518
 
        from bzrlib.inventory import Inventory, InventoryEntry
519
495
        inv = Inventory()
520
496
        for path, file_id, parent, kind in new_inventory_list:
521
497
            name = os.path.basename(path)
545
521
 
546
522
 
547
523
    def append_revision(self, revision_id):
548
 
        from bzrlib.atomicfile import AtomicFile
549
 
 
550
524
        mutter("add {%s} to revision-history" % revision_id)
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()
 
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
        
560
540
 
561
541
 
562
542
    def get_revision(self, revision_id):
563
543
        """Return the Revision object for a named revision"""
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
 
            
 
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])
575
547
        assert r.revision_id == revision_id
576
548
        return r
577
 
        
578
549
 
579
550
    def get_revision_sha1(self, revision_id):
580
551
        """Hash the stored value of a revision, and return it."""
593
564
        TODO: Perhaps for this and similar methods, take a revision
594
565
               parameter which can be either an integer revno or a
595
566
               string hash."""
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
 
            
 
567
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
568
        return i
601
569
 
602
570
    def get_inventory_sha1(self, inventory_id):
603
571
        """Return the sha1 hash of the inventory entry
607
575
 
608
576
    def get_revision_inventory(self, revision_id):
609
577
        """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.
612
578
        if revision_id == None:
613
 
            from bzrlib.inventory import Inventory
614
579
            return Inventory()
615
580
        else:
616
 
            return self.get_inventory(revision_id)
 
581
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
617
582
 
618
583
 
619
584
    def revision_history(self):
783
748
        True
784
749
        """
785
750
        from bzrlib.progress import ProgressBar
786
 
        try:
787
 
            set
788
 
        except NameError:
789
 
            from sets import Set as set
790
751
 
791
752
        pb = ProgressBar()
792
753
 
793
754
        pb.update('comparing histories')
794
755
        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
 
                
803
756
        revisions = []
804
 
        needed_texts = set()
 
757
        needed_texts = sets.Set()
805
758
        i = 0
806
759
        for rev_id in revision_ids:
807
760
            i += 1
832
785
                    
833
786
        
834
787
    def commit(self, *args, **kw):
 
788
        """Deprecated"""
835
789
        from bzrlib.commit import commit
836
790
        commit(self, *args, **kw)
837
791
        
853
807
 
854
808
        `revision_id` may be None for the null revision, in which case
855
809
        an `EmptyTree` is returned."""
856
 
        from bzrlib.tree import EmptyTree, RevisionTree
857
810
        # TODO: refactor this to use an existing revision object
858
811
        # so we don't need to read it in twice.
859
812
        if revision_id == None:
874
827
 
875
828
        If there are no revisions yet, return an `EmptyTree`.
876
829
        """
877
 
        from bzrlib.tree import EmptyTree, RevisionTree
878
830
        r = self.last_patch()
879
831
        if r == None:
880
832
            return EmptyTree()
998
950
            self.unlock()
999
951
 
1000
952
 
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
 
 
1087
953
 
1088
954
class ScratchBranch(Branch):
1089
955
    """Special test class: a branch that cleans up after itself.
1103
969
 
1104
970
        If any files are listed, they are created in the working copy.
1105
971
        """
1106
 
        from tempfile import mkdtemp
1107
972
        init = False
1108
973
        if base is None:
1109
 
            base = mkdtemp()
 
974
            base = tempfile.mkdtemp()
1110
975
            init = True
1111
976
        Branch.__init__(self, base, init=init)
1112
977
        for d in dirs:
1125
990
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1126
991
        True
1127
992
        """
1128
 
        from shutil import copytree
1129
 
        from tempfile import mkdtemp
1130
 
        base = mkdtemp()
 
993
        base = tempfile.mkdtemp()
1131
994
        os.rmdir(base)
1132
 
        copytree(self.base, base, symlinks=True)
 
995
        shutil.copytree(self.base, base, symlinks=True)
1133
996
        return ScratchBranch(base=base)
1134
997
        
1135
998
    def __del__(self):
1137
1000
 
1138
1001
    def destroy(self):
1139
1002
        """Destroy the test branch, removing the scratch directory."""
1140
 
        from shutil import rmtree
1141
1003
        try:
1142
1004
            if self.base:
1143
1005
                mutter("delete ScratchBranch %s" % self.base)
1144
 
                rmtree(self.base)
 
1006
                shutil.rmtree(self.base)
1145
1007
        except OSError, e:
1146
1008
            # Work around for shutil.rmtree failing on Windows when
1147
1009
            # readonly files are encountered
1149
1011
            for root, dirs, files in os.walk(self.base, topdown=False):
1150
1012
                for name in files:
1151
1013
                    os.chmod(os.path.join(root, name), 0700)
1152
 
            rmtree(self.base)
 
1014
            shutil.rmtree(self.base)
1153
1015
        self.base = None
1154
1016
 
1155
1017
    
1180
1042
    cope with just randomness because running uuidgen every time is
1181
1043
    slow."""
1182
1044
    import re
1183
 
    from binascii import hexlify
1184
 
    from time import time
1185
1045
 
1186
1046
    # get last component
1187
1047
    idx = name.rfind('/')
1199
1059
    name = re.sub(r'[^\w.]', '', name)
1200
1060
 
1201
1061
    s = hexlify(rand_bytes(8))
1202
 
    return '-'.join((name, compact_date(time()), s))
 
1062
    return '-'.join((name, compact_date(time.time()), s))