~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/annotate.py

  • Committer: John Arbash Meinel
  • Date: 2008-08-18 22:34:21 UTC
  • mto: (3606.5.6 1.6)
  • mto: This revision was merged to the branch mainline in revision 3641.
  • Revision ID: john@arbash-meinel.com-20080818223421-todjny24vj4faj4t
Add tests for the fetching behavior.

The proper parameter passed is 'unordered' add an assert for it, and
fix callers that were passing 'unsorted' instead.
Add tests that we make the right get_record_stream call based
on the value of _fetch_uses_deltas.
Fix the fetch request for signatures.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005 by Canonical Ltd
2
 
 
 
1
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
26
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.  
27
27
 
28
28
import sys
29
 
import os
30
29
import time
31
30
 
32
 
import bzrlib.weave
 
31
from bzrlib import (
 
32
    errors,
 
33
    osutils,
 
34
    patiencediff,
 
35
    tsort,
 
36
    )
33
37
from bzrlib.config import extract_email_address
34
 
from bzrlib.errors import BzrError
35
38
 
36
39
 
37
40
def annotate_file(branch, rev_id, file_id, verbose=False, full=False,
38
 
        to_file=None):
 
41
                  to_file=None, show_ids=False):
 
42
    """Annotate file_id at revision rev_id in branch.
 
43
 
 
44
    The branch should already be read_locked() when annotate_file is called.
 
45
 
 
46
    :param branch: The branch to look for revision numbers and history from.
 
47
    :param rev_id: The revision id to annotate.
 
48
    :param file_id: The file_id to annotate.
 
49
    :param verbose: Show all details rather than truncating to ensure
 
50
        reasonable text width.
 
51
    :param full: XXXX Not sure what this does.
 
52
    :param to_file: The file to output the annotation to; if None stdout is
 
53
        used.
 
54
    :param show_ids: Show revision ids in the annotation output.
 
55
    """
39
56
    if to_file is None:
40
57
        to_file = sys.stdout
41
58
 
42
 
    prevanno=''
 
59
    # Handle the show_ids case
 
60
    last_rev_id = None
 
61
    if show_ids:
 
62
        annotations = _annotations(branch.repository, file_id, rev_id)
 
63
        max_origin_len = max(len(origin) for origin, text in annotations)
 
64
        for origin, text in annotations:
 
65
            if full or last_rev_id != origin:
 
66
                this = origin
 
67
            else:
 
68
                this = ''
 
69
            to_file.write('%*s | %s' % (max_origin_len, this, text))
 
70
            last_rev_id = origin
 
71
        return
 
72
 
 
73
    # Calculate the lengths of the various columns
43
74
    annotation = list(_annotate_file(branch, rev_id, file_id))
44
 
    max_origin_len = max(len(origin) for origin in set(x[1] for x in annotation))
45
 
    for (revno_str, author, date_str, line_rev_id, text ) in annotation:
 
75
    if len(annotation) == 0:
 
76
        max_origin_len = max_revno_len = max_revid_len = 0
 
77
    else:
 
78
        max_origin_len = max(len(x[1]) for x in annotation)
 
79
        max_revno_len = max(len(x[0]) for x in annotation)
 
80
        max_revid_len = max(len(x[3]) for x in annotation)
 
81
    if not verbose:
 
82
        max_revno_len = min(max_revno_len, 12)
 
83
    max_revno_len = max(max_revno_len, 3)
 
84
 
 
85
    # Output the annotations
 
86
    prevanno = ''
 
87
    encoding = getattr(to_file, 'encoding', None) or \
 
88
            osutils.get_terminal_encoding()
 
89
    for (revno_str, author, date_str, line_rev_id, text) in annotation:
46
90
        if verbose:
47
 
            anno = '%5s %-*s %8s ' % (revno_str, max_origin_len, author, date_str)
 
91
            anno = '%-*s %-*s %8s ' % (max_revno_len, revno_str,
 
92
                                       max_origin_len, author, date_str)
48
93
        else:
49
 
            anno = "%5s %-7s " % ( revno_str, author[:7] )
50
 
 
51
 
        if anno.lstrip() == "" and full: anno = prevanno
52
 
        print >>to_file, '%s| %s' % (anno, text)
53
 
        prevanno=anno
54
 
 
55
 
def _annotate_file(branch, rev_id, file_id ):
56
 
 
57
 
    rh = branch.revision_history()
58
 
    w = branch.repository.weave_store.get_weave(file_id, 
59
 
        branch.repository.get_transaction())
 
94
            if len(revno_str) > max_revno_len:
 
95
                revno_str = revno_str[:max_revno_len-1] + '>'
 
96
            anno = "%-*s %-7s " % (max_revno_len, revno_str, author[:7])
 
97
        if anno.lstrip() == "" and full:
 
98
            anno = prevanno
 
99
        try:
 
100
            to_file.write(anno)
 
101
        except UnicodeEncodeError:
 
102
            # cmd_annotate should be passing in an 'exact' object, which means
 
103
            # we have a direct handle to sys.stdout or equivalent. It may not
 
104
            # be able to handle the exact Unicode characters, but 'annotate' is
 
105
            # a user function (non-scripting), so shouldn't die because of
 
106
            # unrepresentable annotation characters. So encode using 'replace',
 
107
            # and write them again.
 
108
            to_file.write(anno.encode(encoding, 'replace'))
 
109
        to_file.write('| %s\n' % (text,))
 
110
        prevanno = anno
 
111
 
 
112
 
 
113
def _annotations(repo, file_id, rev_id):
 
114
    """Return the list of (origin_revision_id, line_text) for a revision of a file in a repository."""
 
115
    annotations = repo.texts.annotate((file_id, rev_id))
 
116
    # 
 
117
    return [(key[-1], line) for (key, line) in annotations]
 
118
 
 
119
 
 
120
def _annotate_file(branch, rev_id, file_id):
 
121
    """Yield the origins for each line of a file.
 
122
 
 
123
    This includes detailed information, such as the author name, and
 
124
    date string for the commit, rather than just the revision id.
 
125
    """
 
126
    revision_id_to_revno = branch.get_revision_id_to_revno_map()
 
127
    annotations = _annotations(branch.repository, file_id, rev_id)
60
128
    last_origin = None
61
 
    for origin, text in w.annotate_iter(rev_id):
 
129
    revision_ids = set(o for o, t in annotations)
 
130
    revision_ids = [o for o in revision_ids if 
 
131
                    branch.repository.has_revision(o)]
 
132
    revisions = dict((r.revision_id, r) for r in 
 
133
                     branch.repository.get_revisions(revision_ids))
 
134
    for origin, text in annotations:
62
135
        text = text.rstrip('\r\n')
63
136
        if origin == last_origin:
64
137
            (revno_str, author, date_str) = ('','','')
65
138
        else:
66
139
            last_origin = origin
67
 
            if not branch.repository.has_revision(origin):
 
140
            if origin not in revisions:
68
141
                (revno_str, author, date_str) = ('?','?','?')
69
142
            else:
70
 
                if origin in rh:
71
 
                    revno_str = str(rh.index(origin) + 1)
72
 
                else:
73
 
                    revno_str = 'merge'
74
 
            rev = branch.repository.get_revision(origin)
 
143
                revno_str = '.'.join(str(i) for i in
 
144
                                            revision_id_to_revno[origin])
 
145
            rev = revisions[origin]
75
146
            tz = rev.timezone or 0
76
 
            date_str = time.strftime('%Y%m%d', 
 
147
            date_str = time.strftime('%Y%m%d',
77
148
                                     time.gmtime(rev.timestamp + tz))
78
149
            # a lazy way to get something like the email address
79
150
            # TODO: Get real email address
80
 
            author = rev.committer
 
151
            author = rev.get_apparent_author()
81
152
            try:
82
153
                author = extract_email_address(author)
83
 
            except BzrError:
 
154
            except errors.NoEmailInUsername:
84
155
                pass        # use the whole name
85
156
        yield (revno_str, author, date_str, origin, text)
 
157
 
 
158
 
 
159
def reannotate(parents_lines, new_lines, new_revision_id,
 
160
               _left_matching_blocks=None,
 
161
               heads_provider=None):
 
162
    """Create a new annotated version from new lines and parent annotations.
 
163
    
 
164
    :param parents_lines: List of annotated lines for all parents
 
165
    :param new_lines: The un-annotated new lines
 
166
    :param new_revision_id: The revision-id to associate with new lines
 
167
        (will often be CURRENT_REVISION)
 
168
    :param left_matching_blocks: a hint about which areas are common
 
169
        between the text and its left-hand-parent.  The format is
 
170
        the SequenceMatcher.get_matching_blocks format
 
171
        (start_left, start_right, length_of_match).
 
172
    :param heads_provider: An object which provids a .heads() call to resolve
 
173
        if any revision ids are children of others.
 
174
        If None, then any ancestry disputes will be resolved with
 
175
        new_revision_id
 
176
    """
 
177
    if len(parents_lines) == 0:
 
178
        lines = [(new_revision_id, line) for line in new_lines]
 
179
    elif len(parents_lines) == 1:
 
180
        lines = _reannotate(parents_lines[0], new_lines, new_revision_id,
 
181
                            _left_matching_blocks)
 
182
    elif len(parents_lines) == 2:
 
183
        left = _reannotate(parents_lines[0], new_lines, new_revision_id,
 
184
                           _left_matching_blocks)
 
185
        lines = _reannotate_annotated(parents_lines[1], new_lines,
 
186
                                      new_revision_id, left,
 
187
                                      heads_provider)
 
188
    else:
 
189
        reannotations = [_reannotate(parents_lines[0], new_lines,
 
190
                                     new_revision_id, _left_matching_blocks)]
 
191
        reannotations.extend(_reannotate(p, new_lines, new_revision_id)
 
192
                             for p in parents_lines[1:])
 
193
        lines = []
 
194
        for annos in zip(*reannotations):
 
195
            origins = set(a for a, l in annos)
 
196
            if len(origins) == 1:
 
197
                # All the parents agree, so just return the first one
 
198
                lines.append(annos[0])
 
199
            else:
 
200
                line = annos[0][1]
 
201
                if len(origins) == 2 and new_revision_id in origins:
 
202
                    origins.remove(new_revision_id)
 
203
                if len(origins) == 1:
 
204
                    lines.append((origins.pop(), line))
 
205
                else:
 
206
                    lines.append((new_revision_id, line))
 
207
    return lines
 
208
 
 
209
 
 
210
def _reannotate(parent_lines, new_lines, new_revision_id,
 
211
                matching_blocks=None):
 
212
    new_cur = 0
 
213
    if matching_blocks is None:
 
214
        plain_parent_lines = [l for r, l in parent_lines]
 
215
        matcher = patiencediff.PatienceSequenceMatcher(None,
 
216
            plain_parent_lines, new_lines)
 
217
        matching_blocks = matcher.get_matching_blocks()
 
218
    lines = []
 
219
    for i, j, n in matching_blocks:
 
220
        for line in new_lines[new_cur:j]:
 
221
            lines.append((new_revision_id, line))
 
222
        lines.extend(parent_lines[i:i+n])
 
223
        new_cur = j + n
 
224
    return lines
 
225
 
 
226
 
 
227
def _get_matching_blocks(old, new):
 
228
    matcher = patiencediff.PatienceSequenceMatcher(None,
 
229
        old, new)
 
230
    return matcher.get_matching_blocks()
 
231
 
 
232
 
 
233
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
 
234
                                     child_lines, start_child, end_child,
 
235
                                     right_lines, start_right, end_right,
 
236
                                     heads_provider, revision_id):
 
237
    """Find lines in plain_right_lines that match the existing lines.
 
238
 
 
239
    :param output_lines: Append final annotated lines to this list
 
240
    :param plain_child_lines: The unannotated new lines for the child text
 
241
    :param child_lines: Lines for the child text which have been annotated
 
242
        for the left parent
 
243
    :param start_child: Position in plain_child_lines and child_lines to start the
 
244
        match searching
 
245
    :param end_child: Last position in plain_child_lines and child_lines to search
 
246
        for a match
 
247
    :param right_lines: The annotated lines for the whole text for the right
 
248
        parent
 
249
    :param start_right: Position in right_lines to start the match
 
250
    :param end_right: Last position in right_lines to search for a match
 
251
    :param heads_provider: When parents disagree on the lineage of a line, we
 
252
        need to check if one side supersedes the other
 
253
    :param revision_id: The label to give if a line should be labeled 'tip'
 
254
    """
 
255
    output_extend = output_lines.extend
 
256
    output_append = output_lines.append
 
257
    # We need to see if any of the unannotated lines match
 
258
    plain_right_subset = [l for a,l in right_lines[start_right:end_right]]
 
259
    plain_child_subset = plain_child_lines[start_child:end_child]
 
260
    match_blocks = _get_matching_blocks(plain_right_subset, plain_child_subset)
 
261
 
 
262
    last_child_idx = 0
 
263
 
 
264
    for right_idx, child_idx, match_len in match_blocks:
 
265
        # All the lines that don't match are just passed along
 
266
        if child_idx > last_child_idx:
 
267
            output_extend(child_lines[start_child + last_child_idx
 
268
                                      :start_child + child_idx])
 
269
        for offset in xrange(match_len):
 
270
            left = child_lines[start_child+child_idx+offset]
 
271
            right = right_lines[start_right+right_idx+offset]
 
272
            if left[0] == right[0]:
 
273
                # The annotations match, just return the left one
 
274
                output_append(left)
 
275
            elif left[0] == revision_id:
 
276
                # The left parent marked this as unmatched, so let the
 
277
                # right parent claim it
 
278
                output_append(right)
 
279
            else:
 
280
                # Left and Right both claim this line
 
281
                if heads_provider is None:
 
282
                    output_append((revision_id, left[1]))
 
283
                else:
 
284
                    heads = heads_provider.heads((left[0], right[0]))
 
285
                    if len(heads) == 1:
 
286
                        output_append((iter(heads).next(), left[1]))
 
287
                    else:
 
288
                        # Both claim different origins, sort lexicographically
 
289
                        # so that we always get a stable result.
 
290
                        output_append(sorted([left, right])[0])
 
291
        last_child_idx = child_idx + match_len
 
292
 
 
293
 
 
294
def _reannotate_annotated(right_parent_lines, new_lines, new_revision_id,
 
295
                          annotated_lines, heads_provider):
 
296
    """Update the annotations for a node based on another parent.
 
297
 
 
298
    :param right_parent_lines: A list of annotated lines for the right-hand
 
299
        parent.
 
300
    :param new_lines: The unannotated new lines.
 
301
    :param new_revision_id: The revision_id to attribute to lines which are not
 
302
        present in either parent.
 
303
    :param annotated_lines: A list of annotated lines. This should be the
 
304
        annotation of new_lines based on parents seen so far.
 
305
    :param heads_provider: When parents disagree on the lineage of a line, we
 
306
        need to check if one side supersedes the other.
 
307
    """
 
308
    if len(new_lines) != len(annotated_lines):
 
309
        raise AssertionError("mismatched new_lines and annotated_lines")
 
310
    # First compare the newly annotated lines with the right annotated lines.
 
311
    # Lines which were not changed in left or right should match. This tends to
 
312
    # be the bulk of the lines, and they will need no further processing.
 
313
    lines = []
 
314
    lines_extend = lines.extend
 
315
    last_right_idx = 0 # The line just after the last match from the right side
 
316
    last_left_idx = 0
 
317
    matching_left_and_right = _get_matching_blocks(right_parent_lines,
 
318
                                                   annotated_lines)
 
319
    for right_idx, left_idx, match_len in matching_left_and_right:
 
320
        # annotated lines from last_left_idx to left_idx did not match the lines from
 
321
        # last_right_idx
 
322
        # to right_idx, the raw lines should be compared to determine what annotations
 
323
        # need to be updated
 
324
        if last_right_idx == right_idx or last_left_idx == left_idx:
 
325
            # One of the sides is empty, so this is a pure insertion
 
326
            lines_extend(annotated_lines[last_left_idx:left_idx])
 
327
        else:
 
328
            # We need to see if any of the unannotated lines match
 
329
            _find_matching_unannotated_lines(lines,
 
330
                                             new_lines, annotated_lines,
 
331
                                             last_left_idx, left_idx,
 
332
                                             right_parent_lines,
 
333
                                             last_right_idx, right_idx,
 
334
                                             heads_provider,
 
335
                                             new_revision_id)
 
336
        last_right_idx = right_idx + match_len
 
337
        last_left_idx = left_idx + match_len
 
338
        # If left and right agree on a range, just push that into the output
 
339
        lines_extend(annotated_lines[left_idx:left_idx + match_len])
 
340
    return lines