~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-08-19 21:39:05 UTC
  • Revision ID: mbp@sourcefrog.net-20050819213905-dcb792daf09efa84
prepare 0.0.6 release

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