~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-06-22 06:37:43 UTC
  • Revision ID: mbp@sourcefrog.net-20050622063743-e395f04c4db8977f
- move old blackbox code from testbzr into bzrlib.selftest.blackbox

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"
305
294
                  'branch-lock'):
306
295
            self.controlfile(f, 'w').write('')
307
296
        mutter('created control directory in ' + self.base)
308
 
 
309
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
297
        Inventory().write_xml(self.controlfile('inventory','w'))
310
298
 
311
299
 
312
300
    def _check_format(self):
331
319
 
332
320
    def read_working_inventory(self):
333
321
        """Read the working inventory."""
334
 
        from bzrlib.inventory import Inventory
335
 
        from bzrlib.xml import unpack_xml
336
 
        from time import time
337
 
        before = time()
 
322
        before = time.time()
 
323
        # ElementTree does its own conversion from UTF-8, so open in
 
324
        # binary.
338
325
        self.lock_read()
339
326
        try:
340
 
            # ElementTree does its own conversion from UTF-8, so open in
341
 
            # binary.
342
 
            inv = unpack_xml(Inventory,
343
 
                                  self.controlfile('inventory', 'rb'))
 
327
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
344
328
            mutter("loaded inventory of %d items in %f"
345
 
                   % (len(inv), time() - before))
 
329
                   % (len(inv), time.time() - before))
346
330
            return inv
347
331
        finally:
348
332
            self.unlock()
354
338
        That is to say, the inventory describing changes underway, that
355
339
        will be committed to the next revision.
356
340
        """
357
 
        from bzrlib.atomicfile import AtomicFile
358
 
        from bzrlib.xml import pack_xml
359
 
        
360
 
        self.lock_write()
361
 
        try:
362
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
363
 
            try:
364
 
                pack_xml(inv, f)
365
 
                f.commit()
366
 
            finally:
367
 
                f.close()
368
 
        finally:
369
 
            self.unlock()
370
 
        
 
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)
371
351
        mutter('wrote working inventory')
372
352
            
373
353
 
401
381
              add all non-ignored children.  Perhaps do that in a
402
382
              higher-level method.
403
383
        """
404
 
        from bzrlib.textui import show_status
405
384
        # TODO: Re-adding a file that is removed in the working copy
406
385
        # should probably put it back with the previous ID.
407
 
        if isinstance(files, basestring):
408
 
            assert(ids is None or isinstance(ids, basestring))
 
386
        if isinstance(files, types.StringTypes):
 
387
            assert(ids is None or isinstance(ids, types.StringTypes))
409
388
            files = [files]
410
389
            if ids is not None:
411
390
                ids = [ids]
443
422
                inv.add_path(f, kind=kind, file_id=file_id)
444
423
 
445
424
                if verbose:
446
 
                    print 'added', quotefn(f)
 
425
                    show_status('A', kind, quotefn(f))
447
426
 
448
427
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
449
428
 
480
459
        is the opposite of add.  Removing it is consistent with most
481
460
        other tools.  Maybe an option.
482
461
        """
483
 
        from bzrlib.textui import show_status
484
462
        ## TODO: Normalize names
485
463
        ## TODO: Remove nested loops; better scalability
486
 
        if isinstance(files, basestring):
 
464
        if isinstance(files, types.StringTypes):
487
465
            files = [files]
488
466
 
489
467
        self.lock_write()
514
492
 
515
493
    # FIXME: this doesn't need to be a branch method
516
494
    def set_inventory(self, new_inventory_list):
517
 
        from bzrlib.inventory import Inventory, InventoryEntry
518
495
        inv = Inventory()
519
496
        for path, file_id, parent, kind in new_inventory_list:
520
497
            name = os.path.basename(path)
544
521
 
545
522
 
546
523
    def append_revision(self, revision_id):
547
 
        from bzrlib.atomicfile import AtomicFile
548
 
 
549
524
        mutter("add {%s} to revision-history" % revision_id)
550
 
        rev_history = self.revision_history() + [revision_id]
551
 
 
552
 
        f = AtomicFile(self.controlfilename('revision-history'))
553
 
        try:
554
 
            for rev_id in rev_history:
555
 
                print >>f, rev_id
556
 
            f.commit()
557
 
        finally:
558
 
            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
        
559
540
 
560
541
 
561
542
    def get_revision(self, revision_id):
562
543
        """Return the Revision object for a named revision"""
563
 
        from bzrlib.revision import Revision
564
 
        from bzrlib.xml import unpack_xml
565
 
 
566
 
        self.lock_read()
567
 
        try:
568
 
            if not revision_id or not isinstance(revision_id, basestring):
569
 
                raise ValueError('invalid revision-id: %r' % revision_id)
570
 
            r = unpack_xml(Revision, self.revision_store[revision_id])
571
 
        finally:
572
 
            self.unlock()
573
 
            
 
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])
574
547
        assert r.revision_id == revision_id
575
548
        return r
576
 
        
577
549
 
578
550
    def get_revision_sha1(self, revision_id):
579
551
        """Hash the stored value of a revision, and return it."""
592
564
        TODO: Perhaps for this and similar methods, take a revision
593
565
               parameter which can be either an integer revno or a
594
566
               string hash."""
595
 
        from bzrlib.inventory import Inventory
596
 
        from bzrlib.xml import unpack_xml
597
 
 
598
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
599
 
            
 
567
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
568
        return i
600
569
 
601
570
    def get_inventory_sha1(self, inventory_id):
602
571
        """Return the sha1 hash of the inventory entry
607
576
    def get_revision_inventory(self, revision_id):
608
577
        """Return inventory of a past revision."""
609
578
        if revision_id == None:
610
 
            from bzrlib.inventory import Inventory
611
579
            return Inventory()
612
580
        else:
613
581
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
780
748
        True
781
749
        """
782
750
        from bzrlib.progress import ProgressBar
783
 
        try:
784
 
            set
785
 
        except NameError:
786
 
            from sets import Set as set
787
751
 
788
752
        pb = ProgressBar()
789
753
 
790
754
        pb.update('comparing histories')
791
755
        revision_ids = self.missing_revisions(other, stop_revision)
792
 
 
793
 
        if hasattr(other.revision_store, "prefetch"):
794
 
            other.revision_store.prefetch(revision_ids)
795
 
        if hasattr(other.inventory_store, "prefetch"):
796
 
            inventory_ids = [other.get_revision(r).inventory_id
797
 
                             for r in revision_ids]
798
 
            other.inventory_store.prefetch(inventory_ids)
799
 
                
800
756
        revisions = []
801
 
        needed_texts = set()
 
757
        needed_texts = sets.Set()
802
758
        i = 0
803
759
        for rev_id in revision_ids:
804
760
            i += 1
829
785
                    
830
786
        
831
787
    def commit(self, *args, **kw):
 
788
        """Deprecated"""
832
789
        from bzrlib.commit import commit
833
790
        commit(self, *args, **kw)
834
791
        
850
807
 
851
808
        `revision_id` may be None for the null revision, in which case
852
809
        an `EmptyTree` is returned."""
853
 
        from bzrlib.tree import EmptyTree, RevisionTree
854
810
        # TODO: refactor this to use an existing revision object
855
811
        # so we don't need to read it in twice.
856
812
        if revision_id == None:
871
827
 
872
828
        If there are no revisions yet, return an `EmptyTree`.
873
829
        """
874
 
        from bzrlib.tree import EmptyTree, RevisionTree
875
830
        r = self.last_patch()
876
831
        if r == None:
877
832
            return EmptyTree()
995
950
            self.unlock()
996
951
 
997
952
 
998
 
    def revert(self, filenames, old_tree=None, backups=True):
999
 
        """Restore selected files to the versions from a previous tree.
1000
 
 
1001
 
        backups
1002
 
            If true (default) backups are made of files before
1003
 
            they're renamed.
1004
 
        """
1005
 
        from bzrlib.errors import NotVersionedError, BzrError
1006
 
        from bzrlib.atomicfile import AtomicFile
1007
 
        from bzrlib.osutils import backup_file
1008
 
        
1009
 
        inv = self.read_working_inventory()
1010
 
        if old_tree is None:
1011
 
            old_tree = self.basis_tree()
1012
 
        old_inv = old_tree.inventory
1013
 
 
1014
 
        nids = []
1015
 
        for fn in filenames:
1016
 
            file_id = inv.path2id(fn)
1017
 
            if not file_id:
1018
 
                raise NotVersionedError("not a versioned file", fn)
1019
 
            if not old_inv.has_id(file_id):
1020
 
                raise BzrError("file not present in old tree", fn, file_id)
1021
 
            nids.append((fn, file_id))
1022
 
            
1023
 
        # TODO: Rename back if it was previously at a different location
1024
 
 
1025
 
        # TODO: If given a directory, restore the entire contents from
1026
 
        # the previous version.
1027
 
 
1028
 
        # TODO: Make a backup to a temporary file.
1029
 
 
1030
 
        # TODO: If the file previously didn't exist, delete it?
1031
 
        for fn, file_id in nids:
1032
 
            backup_file(fn)
1033
 
            
1034
 
            f = AtomicFile(fn, 'wb')
1035
 
            try:
1036
 
                f.write(old_tree.get_file(file_id).read())
1037
 
                f.commit()
1038
 
            finally:
1039
 
                f.close()
1040
 
 
1041
 
 
1042
953
 
1043
954
class ScratchBranch(Branch):
1044
955
    """Special test class: a branch that cleans up after itself.
1058
969
 
1059
970
        If any files are listed, they are created in the working copy.
1060
971
        """
1061
 
        from tempfile import mkdtemp
1062
972
        init = False
1063
973
        if base is None:
1064
 
            base = mkdtemp()
 
974
            base = tempfile.mkdtemp()
1065
975
            init = True
1066
976
        Branch.__init__(self, base, init=init)
1067
977
        for d in dirs:
1080
990
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1081
991
        True
1082
992
        """
1083
 
        from shutil import copytree
1084
 
        from tempfile import mkdtemp
1085
 
        base = mkdtemp()
 
993
        base = tempfile.mkdtemp()
1086
994
        os.rmdir(base)
1087
 
        copytree(self.base, base, symlinks=True)
 
995
        shutil.copytree(self.base, base, symlinks=True)
1088
996
        return ScratchBranch(base=base)
1089
997
        
1090
998
    def __del__(self):
1092
1000
 
1093
1001
    def destroy(self):
1094
1002
        """Destroy the test branch, removing the scratch directory."""
1095
 
        from shutil import rmtree
1096
1003
        try:
1097
1004
            if self.base:
1098
1005
                mutter("delete ScratchBranch %s" % self.base)
1099
 
                rmtree(self.base)
 
1006
                shutil.rmtree(self.base)
1100
1007
        except OSError, e:
1101
1008
            # Work around for shutil.rmtree failing on Windows when
1102
1009
            # readonly files are encountered
1104
1011
            for root, dirs, files in os.walk(self.base, topdown=False):
1105
1012
                for name in files:
1106
1013
                    os.chmod(os.path.join(root, name), 0700)
1107
 
            rmtree(self.base)
 
1014
            shutil.rmtree(self.base)
1108
1015
        self.base = None
1109
1016
 
1110
1017
    
1135
1042
    cope with just randomness because running uuidgen every time is
1136
1043
    slow."""
1137
1044
    import re
1138
 
    from binascii import hexlify
1139
 
    from time import time
1140
1045
 
1141
1046
    # get last component
1142
1047
    idx = name.rfind('/')
1154
1059
    name = re.sub(r'[^\w.]', '', name)
1155
1060
 
1156
1061
    s = hexlify(rand_bytes(8))
1157
 
    return '-'.join((name, compact_date(time()), s))
 
1062
    return '-'.join((name, compact_date(time.time()), s))