~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revision.py

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import bzrlib.errors
19
 
 
20
 
 
21
 
class RevisionReference(object):
 
18
 
 
19
 
 
20
from xml import XMLMixin
 
21
 
 
22
try:
 
23
    from cElementTree import Element, ElementTree, SubElement
 
24
except ImportError:
 
25
    from elementtree.ElementTree import Element, ElementTree, SubElement
 
26
 
 
27
from errors import BzrError
 
28
 
 
29
 
 
30
class RevisionReference:
22
31
    """
23
32
    Reference to a stored revision.
24
33
 
26
35
    """
27
36
    revision_id = None
28
37
    revision_sha1 = None
29
 
    def __init__(self, revision_id, revision_sha1=None):
 
38
    def __init__(self, revision_id, revision_sha1):
30
39
        if revision_id == None \
31
40
           or isinstance(revision_id, basestring):
32
41
            self.revision_id = revision_id
42
51
                
43
52
 
44
53
 
45
 
class Revision(object):
 
54
class Revision(XMLMixin):
46
55
    """Single revision on a branch.
47
56
 
48
57
    Revisions may know their revision_hash, but only once they've been
50
59
    into the file it describes.
51
60
 
52
61
    After bzr 0.0.5 revisions are allowed to have multiple parents.
 
62
    To support old clients this is written out in a slightly redundant
 
63
    form: the first parent as the predecessor.  This will eventually
 
64
    be dropped.
53
65
 
54
66
    parents
55
67
        List of parent revisions, each is a RevisionReference.
66
78
        self.__dict__.update(args)
67
79
        self.parents = []
68
80
 
 
81
    def _get_precursor(self):
 
82
        from warnings import warn
 
83
        warn("Revision.precursor is deprecated", stacklevel=2)
 
84
        if self.parents:
 
85
            return self.parents[0].revision_id
 
86
        else:
 
87
            return None
 
88
 
 
89
 
 
90
    def _get_precursor_sha1(self):
 
91
        from warnings import warn
 
92
        warn("Revision.precursor_sha1 is deprecated", stacklevel=2)
 
93
        if self.parents:
 
94
            return self.parents[0].revision_sha1
 
95
        else:
 
96
            return None    
 
97
 
 
98
 
 
99
    def _fail(self):
 
100
        raise Exception("can't assign to precursor anymore")
 
101
 
 
102
 
 
103
    precursor = property(_get_precursor, _fail, _fail)
 
104
    precursor_sha1 = property(_get_precursor_sha1, _fail, _fail)
 
105
 
 
106
 
69
107
 
70
108
    def __repr__(self):
71
109
        return "<Revision id %s>" % self.revision_id
72
110
 
73
 
    def __eq__(self, other):
74
 
        if not isinstance(other, Revision):
75
 
            return False
76
 
        return (self.inventory_id == other.inventory_id
77
 
                and self.inventory_sha1 == other.inventory_sha1
78
 
                and self.revision_id == other.revision_id
79
 
                and self.timestamp == other.timestamp
80
 
                and self.message == other.message
81
 
                and self.timezone == other.timezone
82
 
                and self.committer == other.committer)
83
 
 
84
 
    def __ne__(self, other):
85
 
        return not self.__eq__(other)
86
 
 
87
 
        
88
 
 
89
 
REVISION_ID_RE = None
90
 
 
91
 
def validate_revision_id(rid):
92
 
    """Check rid is syntactically valid for a revision id."""
93
 
    global REVISION_ID_RE
94
 
    if not REVISION_ID_RE:
95
 
        import re
96
 
        REVISION_ID_RE = re.compile('[\w.-]+@[\w.-]+--?\d+--?[0-9a-f]+\Z')
97
 
 
98
 
    if not REVISION_ID_RE.match(rid):
99
 
        raise ValueError("malformed revision-id %r" % rid)
100
 
 
101
 
def is_ancestor(revision_id, candidate_id, revision_source):
102
 
    """Return true if candidate_id is an ancestor of revision_id.
103
 
    A false negative will be returned if any intermediate descendent of
104
 
    candidate_id is not present in any of the revision_sources.
105
 
    
106
 
    revisions_source is an object supporting a get_revision operation that
107
 
    behaves like Branch's.
108
 
    """
109
 
 
110
 
    for ancestor_id, distance in iter_ancestors(revision_id, revision_source):
111
 
        if ancestor_id == candidate_id:
112
 
            return True
113
 
    return False
114
 
 
115
 
def iter_ancestors(revision_id, revision_source, only_present=False):
116
 
    ancestors = (revision_id,)
117
 
    distance = 0
118
 
    while len(ancestors) > 0:
119
 
        new_ancestors = []
120
 
        for ancestor in ancestors:
121
 
            if not only_present:
122
 
                yield ancestor, distance
123
 
            try:
124
 
                revision = revision_source.get_revision(ancestor)
125
 
            except bzrlib.errors.NoSuchRevision, e:
126
 
                if e.revision == revision_id:
127
 
                    raise 
128
 
                else:
129
 
                    continue
130
 
            if only_present:
131
 
                yield ancestor, distance
132
 
            new_ancestors.extend([p.revision_id for p in revision.parents])
133
 
        ancestors = new_ancestors
134
 
        distance += 1
135
 
 
136
 
 
137
 
def find_present_ancestors(revision_id, revision_source):
138
 
    """Return the ancestors of a revision present in a branch.
139
 
 
140
 
    It's possible that a branch won't have the complete ancestry of
141
 
    one of its revisions.  
142
 
 
143
 
    """
144
 
    found_ancestors = {}
145
 
    anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
146
 
                         only_present=True))
147
 
    for anc_order, (anc_id, anc_distance) in anc_iter:
148
 
        if not found_ancestors.has_key(anc_id):
149
 
            found_ancestors[anc_id] = (anc_order, anc_distance)
150
 
    return found_ancestors
151
 
    
152
 
 
153
 
def __get_closest(intersection):
154
 
    intersection.sort()
155
 
    matches = [] 
156
 
    for entry in intersection:
157
 
        if entry[0] == intersection[0][0]:
158
 
            matches.append(entry[2])
159
 
    return matches
160
 
 
161
 
 
162
 
def common_ancestor(revision_a, revision_b, revision_source):
163
 
    """Find the ancestor common to both revisions that is closest to both.
164
 
    """
165
 
    from bzrlib.trace import mutter
166
 
    a_ancestors = find_present_ancestors(revision_a, revision_source)
167
 
    b_ancestors = find_present_ancestors(revision_b, revision_source)
168
 
    a_intersection = []
169
 
    b_intersection = []
170
 
    # a_order is used as a tie-breaker when two equally-good bases are found
171
 
    for revision, (a_order, a_distance) in a_ancestors.iteritems():
172
 
        if b_ancestors.has_key(revision):
173
 
            a_intersection.append((a_distance, a_order, revision))
174
 
            b_intersection.append((b_ancestors[revision][1], a_order, revision))
175
 
    mutter("a intersection: %r" % a_intersection)
176
 
    mutter("b intersection: %r" % b_intersection)
177
 
 
178
 
    a_closest = __get_closest(a_intersection)
179
 
    if len(a_closest) == 0:
180
 
        return None
181
 
    b_closest = __get_closest(b_intersection)
182
 
    assert len(b_closest) != 0
183
 
    mutter ("a_closest %r" % a_closest)
184
 
    mutter ("b_closest %r" % b_closest)
185
 
    if a_closest[0] in b_closest:
186
 
        return a_closest[0]
187
 
    elif b_closest[0] in a_closest:
188
 
        return b_closest[0]
189
 
    else:
190
 
        raise bzrlib.errors.AmbiguousBase((a_closest[0], b_closest[0]))
191
 
    return a_closest[0]
192
 
 
193
 
class MultipleRevisionSources(object):
194
 
    """Proxy that looks in multiple branches for revisions."""
195
 
    def __init__(self, *args):
196
 
        object.__init__(self)
197
 
        assert len(args) != 0
198
 
        self._revision_sources = args
199
 
 
200
 
    def get_revision(self, revision_id):
201
 
        for source in self._revision_sources:
202
 
            try:
203
 
                return source.get_revision(revision_id)
204
 
            except bzrlib.errors.NoSuchRevision, e:
205
 
                pass
206
 
        raise e
207
 
 
208
 
def get_intervening_revisions(ancestor_id, rev_id, rev_source, 
209
 
                              revision_history=None):
210
 
    """Find the longest line of descent from maybe_ancestor to revision.
211
 
    Revision history is followed where possible.
212
 
 
213
 
    If ancestor_id == rev_id, list will be empty.
214
 
    Otherwise, rev_id will be the last entry.  ancestor_id will never appear.
215
 
    If ancestor_id is not an ancestor, NotAncestor will be thrown
216
 
    """
217
 
    [rev_source.get_revision(r) for r in (ancestor_id, rev_id)]
218
 
    if ancestor_id == rev_id:
219
 
        return []
220
 
    def historical_lines(line):
221
 
        """Return a tuple of historical/non_historical lines, for sorting.
222
 
        The non_historical count is negative, since non_historical lines are
223
 
        a bad thing.
224
 
        """
225
 
        good_count = 0
226
 
        bad_count = 0
227
 
        for revision in line:
228
 
            if revision in revision_history:
229
 
                good_count += 1
230
 
            else:
231
 
                bad_count -= 1
232
 
        return good_count, bad_count
233
 
    active = [[rev_id]]
234
 
    successful_lines = []
235
 
    while len(active) > 0:
236
 
        new_active = []
237
 
        for line in active:
238
 
            parent_ids = [p.revision_id for p in 
239
 
                          rev_source.get_revision(line[-1]).parents]
240
 
            for parent in parent_ids:
241
 
                line_copy = line[:]
242
 
                if parent == ancestor_id:
243
 
                    successful_lines.append(line_copy)
244
 
                else:
245
 
                    line_copy.append(parent)
246
 
                    new_active.append(line_copy)
247
 
        active = new_active
248
 
    if len(successful_lines) == 0:
249
 
        raise bzrlib.errors.NotAncestor(rev_id, ancestor_id)
250
 
    for line in successful_lines:
251
 
        line.reverse()
252
 
    if revision_history is not None:
253
 
        by_historical_lines = []
254
 
        for line in successful_lines:
255
 
            count = historical_lines(line)
256
 
            by_historical_lines.append((count, line))
257
 
        by_historical_lines.sort()
258
 
        if by_historical_lines[-1][0][0] > 0:
259
 
            return by_historical_lines[-1][1]
260
 
    assert len(successful_lines)
261
 
    successful_lines.sort(cmp, len)
262
 
    return successful_lines[-1]
 
111
        
 
112
    def to_element(self):
 
113
        root = Element('revision',
 
114
                       committer = self.committer,
 
115
                       timestamp = '%.9f' % self.timestamp,
 
116
                       revision_id = self.revision_id,
 
117
                       inventory_id = self.inventory_id,
 
118
                       inventory_sha1 = self.inventory_sha1,
 
119
                       )
 
120
        if self.timezone:
 
121
            root.set('timezone', str(self.timezone))
 
122
        root.text = '\n'
 
123
        
 
124
        msg = SubElement(root, 'message')
 
125
        msg.text = self.message
 
126
        msg.tail = '\n'
 
127
 
 
128
        if self.parents:
 
129
            # first parent stored as precursor for compatability with 0.0.5 and
 
130
            # earlier
 
131
            pr = self.parents[0]
 
132
            assert pr.revision_id
 
133
            root.set('precursor', pr.revision_id)
 
134
            if pr.revision_sha1:
 
135
                root.set('precursor_sha1', pr.revision_sha1)
 
136
                
 
137
        if self.parents:
 
138
            pelts = SubElement(root, 'parents')
 
139
            pelts.tail = pelts.text = '\n'
 
140
            for rr in self.parents:
 
141
                assert isinstance(rr, RevisionReference)
 
142
                p = SubElement(pelts, 'revision_ref')
 
143
                p.tail = '\n'
 
144
                assert rr.revision_id
 
145
                p.set('revision_id', rr.revision_id)
 
146
                if rr.revision_sha1:
 
147
                    p.set('revision_sha1', rr.revision_sha1)
 
148
 
 
149
        return root
 
150
 
 
151
 
 
152
    def from_element(cls, elt):
 
153
        return unpack_revision(elt)
 
154
 
 
155
    from_element = classmethod(from_element)
 
156
 
 
157
 
 
158
 
 
159
def unpack_revision(elt):
 
160
    """Convert XML element into Revision object."""
 
161
    # <changeset> is deprecated...
 
162
    if elt.tag not in ('revision', 'changeset'):
 
163
        raise BzrError("unexpected tag in revision file: %r" % elt)
 
164
 
 
165
    rev = Revision(committer = elt.get('committer'),
 
166
                   timestamp = float(elt.get('timestamp')),
 
167
                   revision_id = elt.get('revision_id'),
 
168
                   inventory_id = elt.get('inventory_id'),
 
169
                   inventory_sha1 = elt.get('inventory_sha1')
 
170
                   )
 
171
 
 
172
    precursor = elt.get('precursor')
 
173
    precursor_sha1 = elt.get('precursor_sha1')
 
174
 
 
175
    pelts = elt.find('parents')
 
176
 
 
177
    if precursor:
 
178
        # revisions written prior to 0.0.5 have a single precursor
 
179
        # give as an attribute
 
180
        rev_ref = RevisionReference(precursor, precursor_sha1)
 
181
        rev.parents.append(rev_ref)
 
182
    elif pelts:
 
183
        for p in pelts:
 
184
            assert p.tag == 'revision_ref', \
 
185
                   "bad parent node tag %r" % p.tag
 
186
            rev_ref = RevisionReference(p.get('revision_id'),
 
187
                                        p.get('revision_sha1'))
 
188
            rev.parents.append(rev_ref)
 
189
 
 
190
    v = elt.get('timezone')
 
191
    rev.timezone = v and int(v)
 
192
 
 
193
    rev.message = elt.findtext('message') # text of <message>
 
194
    return rev