~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-04 12:26:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050704122602-69901910521e62c3
- check command checks that all inventory-ids are the same as in the revision.

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
19
 
import os
 
18
import sys, os
20
19
 
21
20
import bzrlib
22
21
from bzrlib.trace import mutter, note
23
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
 
     splitpath, \
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
25
23
     sha_file, appendpath, file_kind
26
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
27
 
import bzrlib.errors
28
 
from bzrlib.textui import show_status
29
 
from bzrlib.revision import Revision
30
 
from bzrlib.xml import unpack_xml
31
 
from bzrlib.delta import compare_trees
32
 
from bzrlib.tree import EmptyTree, RevisionTree
33
 
        
 
24
from bzrlib.errors import BzrError
 
25
 
34
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
35
27
## TODO: Maybe include checks for common corruption of newlines, etc?
36
28
 
37
29
 
38
 
# TODO: Some operations like log might retrieve the same revisions
39
 
# repeatedly to calculate deltas.  We could perhaps have a weakref
40
 
# cache in memory to make this faster.
41
 
 
42
 
# TODO: please move the revision-string syntax stuff out of the branch
43
 
# object; it's clutter
44
 
 
45
30
 
46
31
def find_branch(f, **args):
47
32
    if f and (f.startswith('http://') or f.startswith('https://')):
133
118
        Exception.__init__(self, "These branches have diverged.")
134
119
 
135
120
 
 
121
class NoSuchRevision(BzrError):
 
122
    def __init__(self, branch, revision):
 
123
        self.branch = branch
 
124
        self.revision = revision
 
125
        msg = "Branch %s has no revision %d" % (branch, revision)
 
126
        BzrError.__init__(self, msg)
 
127
 
 
128
 
136
129
######################################################################
137
130
# branch objects
138
131
 
157
150
    _lock_count = None
158
151
    _lock = None
159
152
    
160
 
    # Map some sort of prefix into a namespace
161
 
    # stuff like "revno:10", "revid:", etc.
162
 
    # This should match a prefix with a function which accepts
163
 
    REVISION_NAMESPACES = {}
164
 
 
165
153
    def __init__(self, base, init=False, find_root=True):
166
154
        """Create new branch object at a particular location.
167
155
 
319
307
            self.controlfile(f, 'w').write('')
320
308
        mutter('created control directory in ' + self.base)
321
309
 
322
 
        # if we want per-tree root ids then this is the place to set
323
 
        # them; they're not needed for now and so ommitted for
324
 
        # simplicity.
325
310
        pack_xml(Inventory(), self.controlfile('inventory','w'))
326
311
 
327
312
 
343
328
                           ['use a different bzr version',
344
329
                            'or remove the .bzr directory and "bzr init" again'])
345
330
 
346
 
    def get_root_id(self):
347
 
        """Return the id of this branches root"""
348
 
        inv = self.read_working_inventory()
349
 
        return inv.root.file_id
350
331
 
351
 
    def set_root_id(self, file_id):
352
 
        inv = self.read_working_inventory()
353
 
        orig_root_id = inv.root.file_id
354
 
        del inv._byid[inv.root.file_id]
355
 
        inv.root.file_id = file_id
356
 
        inv._byid[inv.root.file_id] = inv.root
357
 
        for fid in inv:
358
 
            entry = inv[fid]
359
 
            if entry.parent_id in (None, orig_root_id):
360
 
                entry.parent_id = inv.root.file_id
361
 
        self._write_inventory(inv)
362
332
 
363
333
    def read_working_inventory(self):
364
334
        """Read the working inventory."""
371
341
            # ElementTree does its own conversion from UTF-8, so open in
372
342
            # binary.
373
343
            inv = unpack_xml(Inventory,
374
 
                             self.controlfile('inventory', 'rb'))
 
344
                                  self.controlfile('inventory', 'rb'))
375
345
            mutter("loaded inventory of %d items in %f"
376
346
                   % (len(inv), time() - before))
377
347
            return inv
432
402
              add all non-ignored children.  Perhaps do that in a
433
403
              higher-level method.
434
404
        """
 
405
        from bzrlib.textui import show_status
435
406
        # TODO: Re-adding a file that is removed in the working copy
436
407
        # should probably put it back with the previous ID.
437
408
        if isinstance(files, basestring):
490
461
            # use inventory as it was in that revision
491
462
            file_id = tree.inventory.path2id(file)
492
463
            if not file_id:
493
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
464
                raise BzrError("%r is not present in revision %d" % (file, revno))
494
465
            tree.print_file(file_id)
495
466
        finally:
496
467
            self.unlock()
510
481
        is the opposite of add.  Removing it is consistent with most
511
482
        other tools.  Maybe an option.
512
483
        """
 
484
        from bzrlib.textui import show_status
513
485
        ## TODO: Normalize names
514
486
        ## TODO: Remove nested loops; better scalability
515
487
        if isinstance(files, basestring):
544
516
    # FIXME: this doesn't need to be a branch method
545
517
    def set_inventory(self, new_inventory_list):
546
518
        from bzrlib.inventory import Inventory, InventoryEntry
547
 
        inv = Inventory(self.get_root_id())
 
519
        inv = Inventory()
548
520
        for path, file_id, parent, kind in new_inventory_list:
549
521
            name = os.path.basename(path)
550
522
            if name == "":
572
544
        return self.working_tree().unknowns()
573
545
 
574
546
 
575
 
    def append_revision(self, *revision_ids):
 
547
    def append_revision(self, revision_id):
576
548
        from bzrlib.atomicfile import AtomicFile
577
549
 
578
 
        for revision_id in revision_ids:
579
 
            mutter("add {%s} to revision-history" % revision_id)
580
 
 
581
 
        rev_history = self.revision_history()
582
 
        rev_history.extend(revision_ids)
 
550
        mutter("add {%s} to revision-history" % revision_id)
 
551
        rev_history = self.revision_history() + [revision_id]
583
552
 
584
553
        f = AtomicFile(self.controlfilename('revision-history'))
585
554
        try:
590
559
            f.close()
591
560
 
592
561
 
593
 
    def get_revision_xml(self, revision_id):
594
 
        """Return XML file object for revision object."""
595
 
        if not revision_id or not isinstance(revision_id, basestring):
596
 
            raise InvalidRevisionId(revision_id)
597
 
 
598
 
        self.lock_read()
599
 
        try:
600
 
            try:
601
 
                return self.revision_store[revision_id]
602
 
            except IndexError:
603
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
604
 
        finally:
605
 
            self.unlock()
606
 
 
607
 
 
608
562
    def get_revision(self, revision_id):
609
563
        """Return the Revision object for a named revision"""
610
 
        xml_file = self.get_revision_xml(revision_id)
 
564
        from bzrlib.revision import Revision
 
565
        from bzrlib.xml import unpack_xml
611
566
 
 
567
        self.lock_read()
612
568
        try:
613
 
            r = unpack_xml(Revision, xml_file)
614
 
        except SyntaxError, e:
615
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
616
 
                                         [revision_id,
617
 
                                          str(e)])
 
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()
618
574
            
619
575
        assert r.revision_id == revision_id
620
576
        return r
621
 
 
622
 
 
623
 
    def get_revision_delta(self, revno):
624
 
        """Return the delta for one revision.
625
 
 
626
 
        The delta is relative to its mainline predecessor, or the
627
 
        empty tree for revision 1.
628
 
        """
629
 
        assert isinstance(revno, int)
630
 
        rh = self.revision_history()
631
 
        if not (1 <= revno <= len(rh)):
632
 
            raise InvalidRevisionNumber(revno)
633
 
 
634
 
        # revno is 1-based; list is 0-based
635
 
 
636
 
        new_tree = self.revision_tree(rh[revno-1])
637
 
        if revno == 1:
638
 
            old_tree = EmptyTree()
639
 
        else:
640
 
            old_tree = self.revision_tree(rh[revno-2])
641
 
 
642
 
        return compare_trees(old_tree, new_tree)
643
 
 
644
577
        
645
578
 
646
579
    def get_revision_sha1(self, revision_id):
651
584
        # the revision, (add signatures/remove signatures) and still
652
585
        # have all hash pointers stay consistent.
653
586
        # But for now, just hash the contents.
654
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
587
        return sha_file(self.revision_store[revision_id])
655
588
 
656
589
 
657
590
    def get_inventory(self, inventory_id):
663
596
        from bzrlib.inventory import Inventory
664
597
        from bzrlib.xml import unpack_xml
665
598
 
666
 
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
667
 
 
668
 
 
669
 
    def get_inventory_xml(self, inventory_id):
670
 
        """Get inventory XML as a file object."""
671
 
        return self.inventory_store[inventory_id]
 
599
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
672
600
            
673
601
 
674
602
    def get_inventory_sha1(self, inventory_id):
675
603
        """Return the sha1 hash of the inventory entry
676
604
        """
677
 
        return sha_file(self.get_inventory_xml(inventory_id))
 
605
        return sha_file(self.inventory_store[inventory_id])
678
606
 
679
607
 
680
608
    def get_revision_inventory(self, revision_id):
681
609
        """Return inventory of a past revision."""
682
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
683
 
        # must be the same as its revision, so this is trivial.
684
610
        if revision_id == None:
685
611
            from bzrlib.inventory import Inventory
686
 
            return Inventory(self.get_root_id())
 
612
            return Inventory()
687
613
        else:
688
 
            return self.get_inventory(revision_id)
 
614
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
689
615
 
690
616
 
691
617
    def revision_history(self):
746
672
                return r+1, my_history[r]
747
673
        return None, None
748
674
 
 
675
    def enum_history(self, direction):
 
676
        """Return (revno, revision_id) for history of branch.
 
677
 
 
678
        direction
 
679
            'forward' is from earliest to latest
 
680
            'reverse' is from latest to earliest
 
681
        """
 
682
        rh = self.revision_history()
 
683
        if direction == 'forward':
 
684
            i = 1
 
685
            for rid in rh:
 
686
                yield i, rid
 
687
                i += 1
 
688
        elif direction == 'reverse':
 
689
            i = len(rh)
 
690
            while i > 0:
 
691
                yield i, rh[i-1]
 
692
                i -= 1
 
693
        else:
 
694
            raise ValueError('invalid history direction', direction)
 
695
 
749
696
 
750
697
    def revno(self):
751
698
        """Return current revision number for this branch.
766
713
            return None
767
714
 
768
715
 
769
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
716
    def missing_revisions(self, other, stop_revision=None):
770
717
        """
771
718
        If self and other have not diverged, return a list of the revisions
772
719
        present in other, but missing from self.
810
757
        return other_history[self_len:stop_revision]
811
758
 
812
759
 
813
 
    def update_revisions(self, other, stop_revision=None, revision_ids=None):
 
760
    def update_revisions(self, other, stop_revision=None):
814
761
        """Pull in all new revisions from other branch.
815
762
        
816
763
        >>> from bzrlib.commit import commit
834
781
        True
835
782
        """
836
783
        from bzrlib.progress import ProgressBar
 
784
        try:
 
785
            set
 
786
        except NameError:
 
787
            from sets import Set as set
837
788
 
838
789
        pb = ProgressBar()
839
790
 
840
791
        pb.update('comparing histories')
841
 
        if revision_ids is None:
842
 
            revision_ids = self.missing_revisions(other, stop_revision)
 
792
        revision_ids = self.missing_revisions(other, stop_revision)
843
793
 
844
794
        if hasattr(other.revision_store, "prefetch"):
845
795
            other.revision_store.prefetch(revision_ids)
884
834
        commit(self, *args, **kw)
885
835
        
886
836
 
887
 
    def lookup_revision(self, revision):
888
 
        """Return the revision identifier for a given revision information."""
889
 
        revno, info = self.get_revision_info(revision)
890
 
        return info
891
 
 
892
 
    def get_revision_info(self, revision):
893
 
        """Return (revno, revision id) for revision identifier.
894
 
 
895
 
        revision can be an integer, in which case it is assumed to be revno (though
896
 
            this will translate negative values into positive ones)
897
 
        revision can also be a string, in which case it is parsed for something like
898
 
            'date:' or 'revid:' etc.
899
 
        """
900
 
        if revision is None:
901
 
            return 0, None
902
 
        revno = None
903
 
        try:# Convert to int if possible
904
 
            revision = int(revision)
905
 
        except ValueError:
906
 
            pass
907
 
        revs = self.revision_history()
908
 
        if isinstance(revision, int):
909
 
            if revision == 0:
910
 
                return 0, None
911
 
            # Mabye we should do this first, but we don't need it if revision == 0
912
 
            if revision < 0:
913
 
                revno = len(revs) + revision + 1
914
 
            else:
915
 
                revno = revision
916
 
        elif isinstance(revision, basestring):
917
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
918
 
                if revision.startswith(prefix):
919
 
                    revno = func(self, revs, revision)
920
 
                    break
921
 
            else:
922
 
                raise BzrError('No namespace registered for string: %r' % revision)
923
 
 
924
 
        if revno is None or revno <= 0 or revno > len(revs):
925
 
            raise BzrError("no such revision %s" % revision)
926
 
        return revno, revs[revno-1]
927
 
 
928
 
    def _namespace_revno(self, revs, revision):
929
 
        """Lookup a revision by revision number"""
930
 
        assert revision.startswith('revno:')
931
 
        try:
932
 
            return int(revision[6:])
933
 
        except ValueError:
934
 
            return None
935
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
936
 
 
937
 
    def _namespace_revid(self, revs, revision):
938
 
        assert revision.startswith('revid:')
939
 
        try:
940
 
            return revs.index(revision[6:]) + 1
941
 
        except ValueError:
942
 
            return None
943
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
944
 
 
945
 
    def _namespace_last(self, revs, revision):
946
 
        assert revision.startswith('last:')
947
 
        try:
948
 
            offset = int(revision[5:])
949
 
        except ValueError:
950
 
            return None
951
 
        else:
952
 
            if offset <= 0:
953
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
954
 
            return len(revs) - offset + 1
955
 
    REVISION_NAMESPACES['last:'] = _namespace_last
956
 
 
957
 
    def _namespace_tag(self, revs, revision):
958
 
        assert revision.startswith('tag:')
959
 
        raise BzrError('tag: namespace registered, but not implemented.')
960
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
961
 
 
962
 
    def _namespace_date(self, revs, revision):
963
 
        assert revision.startswith('date:')
964
 
        import datetime
965
 
        # Spec for date revisions:
966
 
        #   date:value
967
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
968
 
        #   it can also start with a '+/-/='. '+' says match the first
969
 
        #   entry after the given date. '-' is match the first entry before the date
970
 
        #   '=' is match the first entry after, but still on the given date.
971
 
        #
972
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
973
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
974
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
975
 
        #       May 13th, 2005 at 0:00
976
 
        #
977
 
        #   So the proper way of saying 'give me all entries for today' is:
978
 
        #       -r {date:+today}:{date:-tomorrow}
979
 
        #   The default is '=' when not supplied
980
 
        val = revision[5:]
981
 
        match_style = '='
982
 
        if val[:1] in ('+', '-', '='):
983
 
            match_style = val[:1]
984
 
            val = val[1:]
985
 
 
986
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
987
 
        if val.lower() == 'yesterday':
988
 
            dt = today - datetime.timedelta(days=1)
989
 
        elif val.lower() == 'today':
990
 
            dt = today
991
 
        elif val.lower() == 'tomorrow':
992
 
            dt = today + datetime.timedelta(days=1)
993
 
        else:
994
 
            import re
995
 
            # This should be done outside the function to avoid recompiling it.
996
 
            _date_re = re.compile(
997
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
998
 
                    r'(,|T)?\s*'
999
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1000
 
                )
1001
 
            m = _date_re.match(val)
1002
 
            if not m or (not m.group('date') and not m.group('time')):
1003
 
                raise BzrError('Invalid revision date %r' % revision)
1004
 
 
1005
 
            if m.group('date'):
1006
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1007
 
            else:
1008
 
                year, month, day = today.year, today.month, today.day
1009
 
            if m.group('time'):
1010
 
                hour = int(m.group('hour'))
1011
 
                minute = int(m.group('minute'))
1012
 
                if m.group('second'):
1013
 
                    second = int(m.group('second'))
1014
 
                else:
1015
 
                    second = 0
1016
 
            else:
1017
 
                hour, minute, second = 0,0,0
1018
 
 
1019
 
            dt = datetime.datetime(year=year, month=month, day=day,
1020
 
                    hour=hour, minute=minute, second=second)
1021
 
        first = dt
1022
 
        last = None
1023
 
        reversed = False
1024
 
        if match_style == '-':
1025
 
            reversed = True
1026
 
        elif match_style == '=':
1027
 
            last = dt + datetime.timedelta(days=1)
1028
 
 
1029
 
        if reversed:
1030
 
            for i in range(len(revs)-1, -1, -1):
1031
 
                r = self.get_revision(revs[i])
1032
 
                # TODO: Handle timezone.
1033
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1034
 
                if first >= dt and (last is None or dt >= last):
1035
 
                    return i+1
1036
 
        else:
1037
 
            for i in range(len(revs)):
1038
 
                r = self.get_revision(revs[i])
1039
 
                # TODO: Handle timezone.
1040
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1041
 
                if first <= dt and (last is None or dt <= last):
1042
 
                    return i+1
1043
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
837
    def lookup_revision(self, revno):
 
838
        """Return revision hash for revision number."""
 
839
        if revno == 0:
 
840
            return None
 
841
 
 
842
        try:
 
843
            # list is 0-based; revisions are 1-based
 
844
            return self.revision_history()[revno-1]
 
845
        except IndexError:
 
846
            raise BzrError("no such revision %s" % revno)
 
847
 
1044
848
 
1045
849
    def revision_tree(self, revision_id):
1046
850
        """Return Tree for a revision on this branch.
1047
851
 
1048
852
        `revision_id` may be None for the null revision, in which case
1049
853
        an `EmptyTree` is returned."""
 
854
        from bzrlib.tree import EmptyTree, RevisionTree
1050
855
        # TODO: refactor this to use an existing revision object
1051
856
        # so we don't need to read it in twice.
1052
857
        if revision_id == None:
1067
872
 
1068
873
        If there are no revisions yet, return an `EmptyTree`.
1069
874
        """
 
875
        from bzrlib.tree import EmptyTree, RevisionTree
1070
876
        r = self.last_patch()
1071
877
        if r == None:
1072
878
            return EmptyTree()
1392
1198
 
1393
1199
    s = hexlify(rand_bytes(8))
1394
1200
    return '-'.join((name, compact_date(time()), s))
1395
 
 
1396
 
 
1397
 
def gen_root_id():
1398
 
    """Return a new tree-root file id."""
1399
 
    return gen_file_id('TREE_ROOT')
1400