~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-16 03:32:44 UTC
  • mfrom: (1185.1.23)
  • mto: (1185.8.2) (974.1.91)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: mbp@sourcefrog.net-20050916033244-18c4f4bcba663e42
- merge in many integration fixes from Robert

  * xml escaping of unprintable characters

  * 'make clean'

  * new, more consistent Branch constructors 

  * RemoteBranch tests against local farmework

  * scott's non-verbose commit fix 

This seems to break this usage though 

  bzr diff -r 1207..1208 ../bzr.robertc-integration

robertc@robertcollins.net-20050915175953-a16fdc627ce7c541

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
     splitpath, \
25
25
     sha_file, appendpath, file_kind
26
26
 
27
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
 
import bzrlib.errors
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
 
28
     DivergedBranches, NotBranchError
29
29
from bzrlib.textui import show_status
30
30
from bzrlib.revision import Revision
31
31
from bzrlib.delta import compare_trees
43
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
44
44
# cache in memory to make this faster.
45
45
 
46
 
# TODO: please move the revision-string syntax stuff out of the branch
47
 
# object; it's clutter
48
 
 
49
 
 
50
 
def find_branch(f, **args):
51
 
    if f and (f.startswith('http://') or f.startswith('https://')):
52
 
        import remotebranch 
53
 
        return remotebranch.RemoteBranch(f, **args)
54
 
    else:
55
 
        return Branch(f, **args)
56
 
 
57
 
 
58
 
def find_cached_branch(f, cache_root, **args):
59
 
    from remotebranch import RemoteBranch
60
 
    br = find_branch(f, **args)
61
 
    def cacheify(br, store_name):
62
 
        from meta_store import CachedStore
63
 
        cache_path = os.path.join(cache_root, store_name)
64
 
        os.mkdir(cache_path)
65
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
66
 
        setattr(br, store_name, new_store)
67
 
 
68
 
    if isinstance(br, RemoteBranch):
69
 
        cacheify(br, 'inventory_store')
70
 
        cacheify(br, 'text_store')
71
 
        cacheify(br, 'revision_store')
72
 
    return br
73
 
 
 
46
def find_branch(*ignored, **ignored_too):
 
47
    # XXX: leave this here for about one release, then remove it
 
48
    raise NotImplementedError('find_branch() is not supported anymore, '
 
49
                              'please use one of the new branch constructors')
74
50
 
75
51
def _relpath(base, path):
76
52
    """Return path relative to base, or raise exception.
94
70
        if tail:
95
71
            s.insert(0, tail)
96
72
    else:
97
 
        from errors import NotBranchError
98
73
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
99
74
 
100
75
    return os.sep.join(s)
128
103
        head, tail = os.path.split(f)
129
104
        if head == f:
130
105
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
106
            raise NotBranchError('%s is not in a branch' % orig_f)
132
107
        f = head
133
108
 
134
109
 
135
110
 
136
 
# XXX: move into bzrlib.errors; subclass BzrError    
137
 
class DivergedBranches(Exception):
138
 
    def __init__(self, branch1, branch2):
139
 
        self.branch1 = branch1
140
 
        self.branch2 = branch2
141
 
        Exception.__init__(self, "These branches have diverged.")
142
 
 
143
111
 
144
112
######################################################################
145
113
# branch objects
148
116
    """Branch holding a history of revisions.
149
117
 
150
118
    base
151
 
        Base directory of the branch.
 
119
        Base directory/url of the branch.
 
120
    """
 
121
    base = None
 
122
 
 
123
    def __init__(self, *ignored, **ignored_too):
 
124
        raise NotImplementedError('The Branch class is abstract')
 
125
 
 
126
    @staticmethod
 
127
    def open(base):
 
128
        """Open an existing branch, rooted at 'base' (url)"""
 
129
        if base and (base.startswith('http://') or base.startswith('https://')):
 
130
            from bzrlib.remotebranch import RemoteBranch
 
131
            return RemoteBranch(base, find_root=False)
 
132
        else:
 
133
            return LocalBranch(base, find_root=False)
 
134
 
 
135
    @staticmethod
 
136
    def open_containing(url):
 
137
        """Open an existing branch, containing url (search upwards for the root)
 
138
        """
 
139
        if url and (url.startswith('http://') or url.startswith('https://')):
 
140
            from bzrlib.remotebranch import RemoteBranch
 
141
            return RemoteBranch(url)
 
142
        else:
 
143
            return LocalBranch(url)
 
144
 
 
145
    @staticmethod
 
146
    def initialize(base):
 
147
        """Create a new branch, rooted at 'base' (url)"""
 
148
        if base and (base.startswith('http://') or base.startswith('https://')):
 
149
            from bzrlib.remotebranch import RemoteBranch
 
150
            return RemoteBranch(base, init=True)
 
151
        else:
 
152
            return LocalBranch(base, init=True)
 
153
 
 
154
    def setup_caching(self, cache_root):
 
155
        """Subclasses that care about caching should override this, and set
 
156
        up cached stores located under cache_root.
 
157
        """
 
158
 
 
159
 
 
160
class LocalBranch(Branch):
 
161
    """A branch stored in the actual filesystem.
 
162
 
 
163
    Note that it's "local" in the context of the filesystem; it doesn't
 
164
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
165
    it's writable, and can be accessed via the normal filesystem API.
152
166
 
153
167
    _lock_mode
154
168
        None, or 'r' or 'w'
160
174
    _lock
161
175
        Lock object from bzrlib.lock.
162
176
    """
163
 
    base = None
 
177
    # We actually expect this class to be somewhat short-lived; part of its
 
178
    # purpose is to try to isolate what bits of the branch logic are tied to
 
179
    # filesystem access, so that in a later step, we can extricate them to
 
180
    # a separarte ("storage") class.
164
181
    _lock_mode = None
165
182
    _lock_count = None
166
183
    _lock = None
167
 
    
168
 
    # Map some sort of prefix into a namespace
169
 
    # stuff like "revno:10", "revid:", etc.
170
 
    # This should match a prefix with a function which accepts
171
 
    REVISION_NAMESPACES = {}
172
184
 
173
185
    def __init__(self, base, init=False, find_root=True):
174
186
        """Create new branch object at a particular location.
175
187
 
176
 
        base -- Base directory for the branch.
 
188
        base -- Base directory for the branch. May be a file:// url.
177
189
        
178
190
        init -- If True, create new control files in a previously
179
191
             unversioned directory.  If False, the branch must already
192
204
        elif find_root:
193
205
            self.base = find_branch_root(base)
194
206
        else:
 
207
            if base.startswith("file://"):
 
208
                base = base[7:]
195
209
            self.base = os.path.realpath(base)
196
210
            if not isdir(self.controlfilename('.')):
197
 
                from errors import NotBranchError
198
211
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
199
212
                                     ['use "bzr init" to initialize a new working tree',
200
213
                                      'current bzr can only operate from top-of-tree'])
214
227
 
215
228
    def __del__(self):
216
229
        if self._lock_mode or self._lock:
217
 
            from warnings import warn
 
230
            from bzrlib.warnings import warn
218
231
            warn("branch %r was not explicitly unlocked" % self)
219
232
            self._lock.unlock()
220
233
 
221
 
 
222
234
    def lock_write(self):
223
235
        if self._lock_mode:
224
236
            if self._lock_mode != 'w':
225
 
                from errors import LockError
 
237
                from bzrlib.errors import LockError
226
238
                raise LockError("can't upgrade to a write lock from %r" %
227
239
                                self._lock_mode)
228
240
            self._lock_count += 1
248
260
                        
249
261
    def unlock(self):
250
262
        if not self._lock_mode:
251
 
            from errors import LockError
 
263
            from bzrlib.errors import LockError
252
264
            raise LockError('branch %r is not locked' % (self))
253
265
 
254
266
        if self._lock_count > 1:
471
483
        """Print `file` to stdout."""
472
484
        self.lock_read()
473
485
        try:
474
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
486
            tree = self.revision_tree(self.get_rev_id(revno))
475
487
            # use inventory as it was in that revision
476
488
            file_id = tree.inventory.path2id(file)
477
489
            if not file_id:
584
596
        try:
585
597
            try:
586
598
                return self.revision_store[revision_id]
587
 
            except KeyError:
 
599
            except (IndexError, KeyError):
588
600
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
589
601
        finally:
590
602
            self.unlock()
695
707
 
696
708
    def common_ancestor(self, other, self_revno=None, other_revno=None):
697
709
        """
698
 
        >>> import commit
 
710
        >>> from bzrlib.commit import commit
699
711
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
700
712
        >>> sb.common_ancestor(sb) == (None, None)
701
713
        True
702
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
714
        >>> commit(sb, "Committing first revision", verbose=False)
703
715
        >>> sb.common_ancestor(sb)[0]
704
716
        1
705
717
        >>> clone = sb.clone()
706
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
718
        >>> commit(sb, "Committing second revision", verbose=False)
707
719
        >>> sb.common_ancestor(sb)[0]
708
720
        2
709
721
        >>> sb.common_ancestor(clone)[0]
710
722
        1
711
 
        >>> commit.commit(clone, "Committing divergent second revision", 
 
723
        >>> commit(clone, "Committing divergent second revision", 
712
724
        ...               verbose=False)
713
725
        >>> sb.common_ancestor(clone)[0]
714
726
        1
809
821
 
810
822
        pb = bzrlib.ui.ui_factory.progress_bar()
811
823
        pb.update('comparing histories')
812
 
 
 
824
        if stop_revision is None:
 
825
            other_revision = other.last_patch()
 
826
        else:
 
827
            other_revision = other.get_rev_id(stop_revision)
 
828
        count = greedy_fetch(self, other, other_revision, pb)[0]
813
829
        try:
814
830
            revision_ids = self.missing_revisions(other, stop_revision)
815
831
        except DivergedBranches, e:
816
832
            try:
817
 
                if stop_revision is None:
818
 
                    end_revision = other.last_patch()
819
833
                revision_ids = get_intervening_revisions(self.last_patch(), 
820
 
                                                         end_revision, other)
 
834
                                                         other_revision, self)
821
835
                assert self.last_patch() not in revision_ids
822
836
            except bzrlib.errors.NotAncestor:
823
837
                raise e
824
838
 
825
 
        if len(revision_ids) > 0:
826
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
827
 
        else:
828
 
            count = 0
829
839
        self.append_revision(*revision_ids)
830
 
        ## note("Added %d revisions." % count)
831
840
        pb.clear()
832
841
 
833
842
    def install_revisions(self, other, revision_ids, pb):
890
899
        commit(self, *args, **kw)
891
900
        
892
901
 
893
 
    def lookup_revision(self, revision):
894
 
        """Return the revision identifier for a given revision information."""
895
 
        revno, info = self._get_revision_info(revision)
896
 
        return info
897
 
 
898
 
 
899
902
    def revision_id_to_revno(self, revision_id):
900
903
        """Given a revision id, return its revno"""
901
904
        history = self.revision_history()
905
908
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
906
909
 
907
910
 
908
 
    def get_revision_info(self, revision):
909
 
        """Return (revno, revision id) for revision identifier.
910
 
 
911
 
        revision can be an integer, in which case it is assumed to be revno (though
912
 
            this will translate negative values into positive ones)
913
 
        revision can also be a string, in which case it is parsed for something like
914
 
            'date:' or 'revid:' etc.
915
 
        """
916
 
        revno, rev_id = self._get_revision_info(revision)
917
 
        if revno is None:
918
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
919
 
        return revno, rev_id
920
 
 
921
911
    def get_rev_id(self, revno, history=None):
922
912
        """Find the revision id of the specified revno."""
923
913
        if revno == 0:
928
918
            raise bzrlib.errors.NoSuchRevision(self, revno)
929
919
        return history[revno - 1]
930
920
 
931
 
    def _get_revision_info(self, revision):
932
 
        """Return (revno, revision id) for revision specifier.
933
 
 
934
 
        revision can be an integer, in which case it is assumed to be revno
935
 
        (though this will translate negative values into positive ones)
936
 
        revision can also be a string, in which case it is parsed for something
937
 
        like 'date:' or 'revid:' etc.
938
 
 
939
 
        A revid is always returned.  If it is None, the specifier referred to
940
 
        the null revision.  If the revid does not occur in the revision
941
 
        history, revno will be None.
942
 
        """
943
 
        
944
 
        if revision is None:
945
 
            return 0, None
946
 
        revno = None
947
 
        try:# Convert to int if possible
948
 
            revision = int(revision)
949
 
        except ValueError:
950
 
            pass
951
 
        revs = self.revision_history()
952
 
        if isinstance(revision, int):
953
 
            if revision < 0:
954
 
                revno = len(revs) + revision + 1
955
 
            else:
956
 
                revno = revision
957
 
            rev_id = self.get_rev_id(revno, revs)
958
 
        elif isinstance(revision, basestring):
959
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
960
 
                if revision.startswith(prefix):
961
 
                    result = func(self, revs, revision)
962
 
                    if len(result) > 1:
963
 
                        revno, rev_id = result
964
 
                    else:
965
 
                        revno = result[0]
966
 
                        rev_id = self.get_rev_id(revno, revs)
967
 
                    break
968
 
            else:
969
 
                raise BzrError('No namespace registered for string: %r' %
970
 
                               revision)
971
 
        else:
972
 
            raise TypeError('Unhandled revision type %s' % revision)
973
 
 
974
 
        if revno is None:
975
 
            if rev_id is None:
976
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
977
 
        return revno, rev_id
978
 
 
979
 
    def _namespace_revno(self, revs, revision):
980
 
        """Lookup a revision by revision number"""
981
 
        assert revision.startswith('revno:')
982
 
        try:
983
 
            return (int(revision[6:]),)
984
 
        except ValueError:
985
 
            return None
986
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
987
 
 
988
 
    def _namespace_revid(self, revs, revision):
989
 
        assert revision.startswith('revid:')
990
 
        rev_id = revision[len('revid:'):]
991
 
        try:
992
 
            return revs.index(rev_id) + 1, rev_id
993
 
        except ValueError:
994
 
            return None, rev_id
995
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
996
 
 
997
 
    def _namespace_last(self, revs, revision):
998
 
        assert revision.startswith('last:')
999
 
        try:
1000
 
            offset = int(revision[5:])
1001
 
        except ValueError:
1002
 
            return (None,)
1003
 
        else:
1004
 
            if offset <= 0:
1005
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
1006
 
            return (len(revs) - offset + 1,)
1007
 
    REVISION_NAMESPACES['last:'] = _namespace_last
1008
 
 
1009
 
    def _namespace_tag(self, revs, revision):
1010
 
        assert revision.startswith('tag:')
1011
 
        raise BzrError('tag: namespace registered, but not implemented.')
1012
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
1013
 
 
1014
 
    def _namespace_date(self, revs, revision):
1015
 
        assert revision.startswith('date:')
1016
 
        import datetime
1017
 
        # Spec for date revisions:
1018
 
        #   date:value
1019
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1020
 
        #   it can also start with a '+/-/='. '+' says match the first
1021
 
        #   entry after the given date. '-' is match the first entry before the date
1022
 
        #   '=' is match the first entry after, but still on the given date.
1023
 
        #
1024
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1025
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1026
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1027
 
        #       May 13th, 2005 at 0:00
1028
 
        #
1029
 
        #   So the proper way of saying 'give me all entries for today' is:
1030
 
        #       -r {date:+today}:{date:-tomorrow}
1031
 
        #   The default is '=' when not supplied
1032
 
        val = revision[5:]
1033
 
        match_style = '='
1034
 
        if val[:1] in ('+', '-', '='):
1035
 
            match_style = val[:1]
1036
 
            val = val[1:]
1037
 
 
1038
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1039
 
        if val.lower() == 'yesterday':
1040
 
            dt = today - datetime.timedelta(days=1)
1041
 
        elif val.lower() == 'today':
1042
 
            dt = today
1043
 
        elif val.lower() == 'tomorrow':
1044
 
            dt = today + datetime.timedelta(days=1)
1045
 
        else:
1046
 
            import re
1047
 
            # This should be done outside the function to avoid recompiling it.
1048
 
            _date_re = re.compile(
1049
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1050
 
                    r'(,|T)?\s*'
1051
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1052
 
                )
1053
 
            m = _date_re.match(val)
1054
 
            if not m or (not m.group('date') and not m.group('time')):
1055
 
                raise BzrError('Invalid revision date %r' % revision)
1056
 
 
1057
 
            if m.group('date'):
1058
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1059
 
            else:
1060
 
                year, month, day = today.year, today.month, today.day
1061
 
            if m.group('time'):
1062
 
                hour = int(m.group('hour'))
1063
 
                minute = int(m.group('minute'))
1064
 
                if m.group('second'):
1065
 
                    second = int(m.group('second'))
1066
 
                else:
1067
 
                    second = 0
1068
 
            else:
1069
 
                hour, minute, second = 0,0,0
1070
 
 
1071
 
            dt = datetime.datetime(year=year, month=month, day=day,
1072
 
                    hour=hour, minute=minute, second=second)
1073
 
        first = dt
1074
 
        last = None
1075
 
        reversed = False
1076
 
        if match_style == '-':
1077
 
            reversed = True
1078
 
        elif match_style == '=':
1079
 
            last = dt + datetime.timedelta(days=1)
1080
 
 
1081
 
        if reversed:
1082
 
            for i in range(len(revs)-1, -1, -1):
1083
 
                r = self.get_revision(revs[i])
1084
 
                # TODO: Handle timezone.
1085
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1086
 
                if first >= dt and (last is None or dt >= last):
1087
 
                    return (i+1,)
1088
 
        else:
1089
 
            for i in range(len(revs)):
1090
 
                r = self.get_revision(revs[i])
1091
 
                # TODO: Handle timezone.
1092
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1093
 
                if first <= dt and (last is None or dt <= last):
1094
 
                    return (i+1,)
1095
 
    REVISION_NAMESPACES['date:'] = _namespace_date
1096
921
 
1097
922
    def revision_tree(self, revision_id):
1098
923
        """Return Tree for a revision on this branch.
1110
935
 
1111
936
    def working_tree(self):
1112
937
        """Return a `Tree` for the working copy."""
1113
 
        from workingtree import WorkingTree
 
938
        from bzrlib.workingtree import WorkingTree
1114
939
        return WorkingTree(self.base, self.read_working_inventory())
1115
940
 
1116
941
 
1381
1206
            raise InvalidRevisionNumber(revno)
1382
1207
        
1383
1208
        
1384
 
 
1385
 
 
1386
 
class ScratchBranch(Branch):
 
1209
        
 
1210
 
 
1211
 
 
1212
class ScratchBranch(LocalBranch):
1387
1213
    """Special test class: a branch that cleans up after itself.
1388
1214
 
1389
1215
    >>> b = ScratchBranch()
1406
1232
        if base is None:
1407
1233
            base = mkdtemp()
1408
1234
            init = True
1409
 
        Branch.__init__(self, base, init=init)
 
1235
        LocalBranch.__init__(self, base, init=init)
1410
1236
        for d in dirs:
1411
1237
            os.mkdir(self.abspath(d))
1412
1238
            
1507
1333
    return gen_file_id('TREE_ROOT')
1508
1334
 
1509
1335
 
1510
 
def pull_loc(branch):
1511
 
    # TODO: Should perhaps just make attribute be 'base' in
1512
 
    # RemoteBranch and Branch?
1513
 
    if hasattr(branch, "baseurl"):
1514
 
        return branch.baseurl
1515
 
    else:
1516
 
        return branch.base
1517
 
 
1518
 
 
1519
1336
def copy_branch(branch_from, to_location, revision=None):
1520
1337
    """Copy branch_from into the existing directory to_location.
1521
1338
 
1527
1344
        The name of a local directory that exists but is empty.
1528
1345
    """
1529
1346
    from bzrlib.merge import merge
1530
 
    from bzrlib.branch import Branch
 
1347
    from bzrlib.revisionspec import RevisionSpec
1531
1348
 
1532
1349
    assert isinstance(branch_from, Branch)
1533
1350
    assert isinstance(to_location, basestring)
1534
1351
    
1535
 
    br_to = Branch(to_location, init=True)
 
1352
    br_to = Branch.initialize(to_location)
1536
1353
    br_to.set_root_id(branch_from.get_root_id())
1537
1354
    if revision is None:
1538
1355
        revno = branch_from.revno()
1539
1356
    else:
1540
 
        revno, rev_id = branch_from.get_revision_info(revision)
 
1357
        revno, rev_id = RevisionSpec(revision).in_history(branch_from)
1541
1358
    br_to.update_revisions(branch_from, stop_revision=revno)
1542
1359
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1543
1360
          check_clean=False, ignore_zero=True)
1544
 
    
1545
 
    from_location = pull_loc(branch_from)
1546
 
    br_to.set_parent(pull_loc(branch_from))
1547
 
 
 
1361
    br_to.set_parent(branch_from.base)
 
1362
    return br_to