~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-09-19 02:33:09 UTC
  • mfrom: (1185.3.27)
  • mto: (1185.1.29)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: aaron.bentley@utoronto.ca-20050919023309-24e8871f7f8b31cf
Merged latest from mpool

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
812
824
        if stop_revision is None:
813
825
            other_revision = other.last_patch()
814
826
        else:
815
 
            other_revision = other.lookup_revision(stop_revision)
 
827
            other_revision = other.get_rev_id(stop_revision)
816
828
        count = greedy_fetch(self, other, other_revision, pb)[0]
817
829
        try:
818
830
            revision_ids = self.missing_revisions(other, stop_revision)
887
899
        commit(self, *args, **kw)
888
900
        
889
901
 
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
 
        assert info is not None or revno == 0, (info, revno)
894
 
        return info
895
 
 
896
902
 
897
903
    def revision_id_to_revno(self, revision_id):
898
904
        """Given a revision id, return its revno"""
903
909
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
904
910
 
905
911
 
906
 
    def get_revision_info(self, revision):
907
 
        """Return (revno, revision id) for revision identifier.
908
 
 
909
 
        revision can be an integer, in which case it is assumed to be revno (though
910
 
            this will translate negative values into positive ones)
911
 
        revision can also be a string, in which case it is parsed for something like
912
 
            'date:' or 'revid:' etc.
913
 
        """
914
 
        revno, rev_id = self._get_revision_info(revision)
915
 
        if revno is None:
916
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
917
 
        return revno, rev_id
918
 
 
919
912
    def get_rev_id(self, revno, history=None):
920
913
        """Find the revision id of the specified revno."""
921
914
        if revno == 0:
926
919
            raise bzrlib.errors.NoSuchRevision(self, revno)
927
920
        return history[revno - 1]
928
921
 
929
 
    def _get_revision_info(self, revision):
930
 
        """Return (revno, revision id) for revision specifier.
931
 
 
932
 
        revision can be an integer, in which case it is assumed to be revno
933
 
        (though this will translate negative values into positive ones)
934
 
        revision can also be a string, in which case it is parsed for something
935
 
        like 'date:' or 'revid:' etc.
936
 
 
937
 
        A revid is always returned.  If it is None, the specifier referred to
938
 
        the null revision.  If the revid does not occur in the revision
939
 
        history, revno will be None.
940
 
        """
941
 
        
942
 
        if revision is None:
943
 
            return 0, None
944
 
        revno = None
945
 
        try:# Convert to int if possible
946
 
            revision = int(revision)
947
 
        except ValueError:
948
 
            pass
949
 
        revs = self.revision_history()
950
 
        if isinstance(revision, int):
951
 
            if revision < 0:
952
 
                revno = len(revs) + revision + 1
953
 
            else:
954
 
                revno = revision
955
 
            rev_id = self.get_rev_id(revno, revs)
956
 
        elif isinstance(revision, basestring):
957
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
958
 
                if revision.startswith(prefix):
959
 
                    result = func(self, revs, revision)
960
 
                    if len(result) > 1:
961
 
                        revno, rev_id = result
962
 
                    else:
963
 
                        revno = result[0]
964
 
                        rev_id = self.get_rev_id(revno, revs)
965
 
                    break
966
 
            else:
967
 
                raise BzrError('No namespace registered for string: %r' %
968
 
                               revision)
969
 
        else:
970
 
            raise TypeError('Unhandled revision type %s' % revision)
971
 
 
972
 
        if revno is None:
973
 
            if rev_id is None:
974
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
975
 
        return revno, rev_id
976
 
 
977
 
    def _namespace_revno(self, revs, revision):
978
 
        """Lookup a revision by revision number"""
979
 
        assert revision.startswith('revno:')
980
 
        try:
981
 
            return (int(revision[6:]),)
982
 
        except ValueError:
983
 
            return None
984
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
985
 
 
986
 
    def _namespace_revid(self, revs, revision):
987
 
        assert revision.startswith('revid:')
988
 
        rev_id = revision[len('revid:'):]
989
 
        try:
990
 
            return revs.index(rev_id) + 1, rev_id
991
 
        except ValueError:
992
 
            return None, rev_id
993
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
994
 
 
995
 
    def _namespace_last(self, revs, revision):
996
 
        assert revision.startswith('last:')
997
 
        try:
998
 
            offset = int(revision[5:])
999
 
        except ValueError:
1000
 
            return (None,)
1001
 
        else:
1002
 
            if offset <= 0:
1003
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
1004
 
            return (len(revs) - offset + 1,)
1005
 
    REVISION_NAMESPACES['last:'] = _namespace_last
1006
 
 
1007
 
    def _namespace_tag(self, revs, revision):
1008
 
        assert revision.startswith('tag:')
1009
 
        raise BzrError('tag: namespace registered, but not implemented.')
1010
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
1011
 
 
1012
 
    def _namespace_date(self, revs, revision):
1013
 
        assert revision.startswith('date:')
1014
 
        import datetime
1015
 
        # Spec for date revisions:
1016
 
        #   date:value
1017
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1018
 
        #   it can also start with a '+/-/='. '+' says match the first
1019
 
        #   entry after the given date. '-' is match the first entry before the date
1020
 
        #   '=' is match the first entry after, but still on the given date.
1021
 
        #
1022
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1023
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1024
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1025
 
        #       May 13th, 2005 at 0:00
1026
 
        #
1027
 
        #   So the proper way of saying 'give me all entries for today' is:
1028
 
        #       -r {date:+today}:{date:-tomorrow}
1029
 
        #   The default is '=' when not supplied
1030
 
        val = revision[5:]
1031
 
        match_style = '='
1032
 
        if val[:1] in ('+', '-', '='):
1033
 
            match_style = val[:1]
1034
 
            val = val[1:]
1035
 
 
1036
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1037
 
        if val.lower() == 'yesterday':
1038
 
            dt = today - datetime.timedelta(days=1)
1039
 
        elif val.lower() == 'today':
1040
 
            dt = today
1041
 
        elif val.lower() == 'tomorrow':
1042
 
            dt = today + datetime.timedelta(days=1)
1043
 
        else:
1044
 
            import re
1045
 
            # This should be done outside the function to avoid recompiling it.
1046
 
            _date_re = re.compile(
1047
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1048
 
                    r'(,|T)?\s*'
1049
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1050
 
                )
1051
 
            m = _date_re.match(val)
1052
 
            if not m or (not m.group('date') and not m.group('time')):
1053
 
                raise BzrError('Invalid revision date %r' % revision)
1054
 
 
1055
 
            if m.group('date'):
1056
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1057
 
            else:
1058
 
                year, month, day = today.year, today.month, today.day
1059
 
            if m.group('time'):
1060
 
                hour = int(m.group('hour'))
1061
 
                minute = int(m.group('minute'))
1062
 
                if m.group('second'):
1063
 
                    second = int(m.group('second'))
1064
 
                else:
1065
 
                    second = 0
1066
 
            else:
1067
 
                hour, minute, second = 0,0,0
1068
 
 
1069
 
            dt = datetime.datetime(year=year, month=month, day=day,
1070
 
                    hour=hour, minute=minute, second=second)
1071
 
        first = dt
1072
 
        last = None
1073
 
        reversed = False
1074
 
        if match_style == '-':
1075
 
            reversed = True
1076
 
        elif match_style == '=':
1077
 
            last = dt + datetime.timedelta(days=1)
1078
 
 
1079
 
        if reversed:
1080
 
            for i in range(len(revs)-1, -1, -1):
1081
 
                r = self.get_revision(revs[i])
1082
 
                # TODO: Handle timezone.
1083
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1084
 
                if first >= dt and (last is None or dt >= last):
1085
 
                    return (i+1,)
1086
 
        else:
1087
 
            for i in range(len(revs)):
1088
 
                r = self.get_revision(revs[i])
1089
 
                # TODO: Handle timezone.
1090
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1091
 
                if first <= dt and (last is None or dt <= last):
1092
 
                    return (i+1,)
1093
 
    REVISION_NAMESPACES['date:'] = _namespace_date
1094
 
 
1095
 
 
1096
 
    def _namespace_ancestor(self, revs, revision):
1097
 
        from revision import common_ancestor, MultipleRevisionSources
1098
 
        other_branch = find_branch(_trim_namespace('ancestor', revision))
1099
 
        revision_a = self.last_patch()
1100
 
        revision_b = other_branch.last_patch()
1101
 
        for r, b in ((revision_a, self), (revision_b, other_branch)):
1102
 
            if r is None:
1103
 
                raise bzrlib.errors.NoCommits(b)
1104
 
        revision_source = MultipleRevisionSources(self, other_branch)
1105
 
        result = common_ancestor(revision_a, revision_b, revision_source)
1106
 
        try:
1107
 
            revno = self.revision_id_to_revno(result)
1108
 
        except bzrlib.errors.NoSuchRevision:
1109
 
            revno = None
1110
 
        return revno,result
1111
 
        
1112
 
 
1113
 
    REVISION_NAMESPACES['ancestor:'] = _namespace_ancestor
1114
922
 
1115
923
    def revision_tree(self, revision_id):
1116
924
        """Return Tree for a revision on this branch.
1128
936
 
1129
937
    def working_tree(self):
1130
938
        """Return a `Tree` for the working copy."""
1131
 
        from workingtree import WorkingTree
 
939
        from bzrlib.workingtree import WorkingTree
1132
940
        return WorkingTree(self.base, self.read_working_inventory())
1133
941
 
1134
942
 
1399
1207
            raise InvalidRevisionNumber(revno)
1400
1208
        
1401
1209
        
1402
 
 
1403
 
 
1404
 
class ScratchBranch(Branch):
 
1210
        
 
1211
 
 
1212
 
 
1213
class ScratchBranch(LocalBranch):
1405
1214
    """Special test class: a branch that cleans up after itself.
1406
1215
 
1407
1216
    >>> b = ScratchBranch()
1424
1233
        if base is None:
1425
1234
            base = mkdtemp()
1426
1235
            init = True
1427
 
        Branch.__init__(self, base, init=init)
 
1236
        LocalBranch.__init__(self, base, init=init)
1428
1237
        for d in dirs:
1429
1238
            os.mkdir(self.abspath(d))
1430
1239
            
1525
1334
    return gen_file_id('TREE_ROOT')
1526
1335
 
1527
1336
 
1528
 
def pull_loc(branch):
1529
 
    # TODO: Should perhaps just make attribute be 'base' in
1530
 
    # RemoteBranch and Branch?
1531
 
    if hasattr(branch, "baseurl"):
1532
 
        return branch.baseurl
1533
 
    else:
1534
 
        return branch.base
1535
 
 
1536
 
 
1537
1337
def copy_branch(branch_from, to_location, revision=None):
1538
1338
    """Copy branch_from into the existing directory to_location.
1539
1339
 
1545
1345
        The name of a local directory that exists but is empty.
1546
1346
    """
1547
1347
    from bzrlib.merge import merge
1548
 
    from bzrlib.branch import Branch
 
1348
    from bzrlib.revisionspec import RevisionSpec
1549
1349
 
1550
1350
    assert isinstance(branch_from, Branch)
1551
1351
    assert isinstance(to_location, basestring)
1552
1352
    
1553
 
    br_to = Branch(to_location, init=True)
 
1353
    br_to = Branch.initialize(to_location)
1554
1354
    br_to.set_root_id(branch_from.get_root_id())
1555
1355
    if revision is None:
1556
1356
        revno = branch_from.revno()
1557
1357
    else:
1558
 
        revno, rev_id = branch_from.get_revision_info(revision)
 
1358
        revno, rev_id = RevisionSpec(revision).in_history(branch_from)
1559
1359
    br_to.update_revisions(branch_from, stop_revision=revno)
1560
1360
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1561
1361
          check_clean=False, ignore_zero=True)
1562
 
    
1563
 
    from_location = pull_loc(branch_from)
1564
 
    br_to.set_parent(pull_loc(branch_from))
 
1362
    br_to.set_parent(branch_from.base)
1565
1363
    return br_to
1566
 
 
1567
 
def _trim_namespace(namespace, spec):
1568
 
    full_namespace = namespace + ':'
1569
 
    assert spec.startswith(full_namespace)
1570
 
    return spec[len(full_namespace):]