~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/annotate.py

  • Committer: John Arbash Meinel
  • Date: 2013-05-19 14:29:37 UTC
  • mfrom: (6437.63.9 2.5)
  • mto: (6437.63.10 2.5)
  • mto: This revision was merged to the branch mainline in revision 6575.
  • Revision ID: john@arbash-meinel.com-20130519142937-21ykz2n2y2f22za9
Merge in the actual 2.5 branch. It seems I failed before

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
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
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
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""File annotate based on weave storage"""
18
18
 
 
19
from __future__ import absolute_import
 
20
 
19
21
# TODO: Choice of more or less verbose formats:
20
 
 
22
#
21
23
# interposed: show more details between blocks of modified lines
22
24
 
23
25
# TODO: Show which revision caused a line to merge into the parent
24
26
 
25
27
# TODO: perhaps abbreviate timescales depending on how recent they are
26
 
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.  
 
28
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.
27
29
 
28
30
import sys
29
31
import time
30
32
 
 
33
from bzrlib.lazy_import import lazy_import
 
34
lazy_import(globals(), """
31
35
from bzrlib import (
32
 
    errors,
33
36
    patiencediff,
34
37
    tsort,
35
38
    )
 
39
""")
 
40
from bzrlib import (
 
41
    errors,
 
42
    osutils,
 
43
    )
36
44
from bzrlib.config import extract_email_address
37
 
 
38
 
 
 
45
from bzrlib.repository import _strip_NULL_ghosts
 
46
from bzrlib.revision import (
 
47
    CURRENT_REVISION,
 
48
    Revision,
 
49
    )
 
50
from bzrlib.symbol_versioning import (
 
51
    deprecated_function,
 
52
    deprecated_in,
 
53
    )
 
54
 
 
55
 
 
56
@deprecated_function(deprecated_in((2, 4, 0)))
39
57
def annotate_file(branch, rev_id, file_id, verbose=False, full=False,
40
58
                  to_file=None, show_ids=False):
 
59
    """Annotate file_id at revision rev_id in branch.
 
60
 
 
61
    The branch should already be read_locked() when annotate_file is called.
 
62
 
 
63
    :param branch: The branch to look for revision numbers and history from.
 
64
    :param rev_id: The revision id to annotate.
 
65
    :param file_id: The file_id to annotate.
 
66
    :param verbose: Show all details rather than truncating to ensure
 
67
        reasonable text width.
 
68
    :param full: XXXX Not sure what this does.
 
69
    :param to_file: The file to output the annotation to; if None stdout is
 
70
        used.
 
71
    :param show_ids: Show revision ids in the annotation output.
 
72
    """
 
73
    tree = branch.repository.revision_tree(rev_id)
 
74
    annotate_file_tree(tree, file_id, to_file, verbose=verbose,
 
75
        full=full, show_ids=show_ids, branch=branch)
 
76
 
 
77
 
 
78
def annotate_file_tree(tree, file_id, to_file, verbose=False, full=False,
 
79
    show_ids=False, branch=None):
 
80
    """Annotate file_id in a tree.
 
81
 
 
82
    The tree should already be read_locked() when annotate_file_tree is called.
 
83
 
 
84
    :param tree: The tree to look for revision numbers and history from.
 
85
    :param file_id: The file_id to annotate.
 
86
    :param to_file: The file to output the annotation to.
 
87
    :param verbose: Show all details rather than truncating to ensure
 
88
        reasonable text width.
 
89
    :param full: XXXX Not sure what this does.
 
90
    :param show_ids: Show revision ids in the annotation output.
 
91
    :param branch: Branch to use for revision revno lookups
 
92
    """
 
93
    if branch is None:
 
94
        branch = tree.branch
41
95
    if to_file is None:
42
96
        to_file = sys.stdout
43
97
 
44
 
    prevanno=''
45
 
    last_rev_id = None
 
98
    # Handle the show_ids case
 
99
    annotations = list(tree.annotate_iter(file_id))
46
100
    if show_ids:
47
 
        w = branch.repository.weave_store.get_weave(file_id,
48
 
            branch.repository.get_transaction())
49
 
        annotations = list(w.annotate_iter(rev_id))
50
 
        max_origin_len = max(len(origin) for origin, text in annotations)
51
 
        for origin, text in annotations:
52
 
            if full or last_rev_id != origin:
53
 
                this = origin
54
 
            else:
55
 
                this = ''
56
 
            to_file.write('%*s | %s' % (max_origin_len, this, text))
57
 
            last_rev_id = origin
58
 
        return
59
 
 
60
 
    annotation = list(_annotate_file(branch, rev_id, file_id))
 
101
        return _show_id_annotations(annotations, to_file, full)
 
102
 
 
103
    if not getattr(tree, "get_revision_id", False):
 
104
        # Create a virtual revision to represent the current tree state.
 
105
        # Should get some more pending commit attributes, like pending tags,
 
106
        # bugfixes etc.
 
107
        current_rev = Revision(CURRENT_REVISION)
 
108
        current_rev.parent_ids = tree.get_parent_ids()
 
109
        try:
 
110
            current_rev.committer = branch.get_config_stack().get('email')
 
111
        except errors.NoWhoami:
 
112
            current_rev.committer = 'local user'
 
113
        current_rev.message = "?"
 
114
        current_rev.timestamp = round(time.time(), 3)
 
115
        current_rev.timezone = osutils.local_time_offset()
 
116
    else:
 
117
        current_rev = None
 
118
    annotation = list(_expand_annotations(annotations, branch,
 
119
        current_rev))
 
120
    _print_annotations(annotation, verbose, to_file, full)
 
121
 
 
122
 
 
123
def _print_annotations(annotation, verbose, to_file, full):
 
124
    """Print annotations to to_file.
 
125
 
 
126
    :param to_file: The file to output the annotation to.
 
127
    :param verbose: Show all details rather than truncating to ensure
 
128
        reasonable text width.
 
129
    :param full: XXXX Not sure what this does.
 
130
    """
61
131
    if len(annotation) == 0:
62
132
        max_origin_len = max_revno_len = max_revid_len = 0
63
133
    else:
64
134
        max_origin_len = max(len(x[1]) for x in annotation)
65
135
        max_revno_len = max(len(x[0]) for x in annotation)
66
136
        max_revid_len = max(len(x[3]) for x in annotation)
67
 
 
68
137
    if not verbose:
69
138
        max_revno_len = min(max_revno_len, 12)
70
139
    max_revno_len = max(max_revno_len, 3)
71
140
 
 
141
    # Output the annotations
 
142
    prevanno = ''
 
143
    encoding = getattr(to_file, 'encoding', None) or \
 
144
            osutils.get_terminal_encoding()
72
145
    for (revno_str, author, date_str, line_rev_id, text) in annotation:
73
146
        if verbose:
74
147
            anno = '%-*s %-*s %8s ' % (max_revno_len, revno_str,
77
150
            if len(revno_str) > max_revno_len:
78
151
                revno_str = revno_str[:max_revno_len-1] + '>'
79
152
            anno = "%-*s %-7s " % (max_revno_len, revno_str, author[:7])
80
 
 
81
 
        if anno.lstrip() == "" and full: anno = prevanno
82
 
        print >>to_file, '%s| %s' % (anno, text)
83
 
        prevanno=anno
84
 
 
85
 
 
86
 
def _annotate_file(branch, rev_id, file_id):
87
 
    """Yield the origins for each line of a file.
88
 
 
89
 
    This includes detailed information, such as the committer name, and
90
 
    date string for the commit, rather than just the revision id.
 
153
        if anno.lstrip() == "" and full:
 
154
            anno = prevanno
 
155
        try:
 
156
            to_file.write(anno)
 
157
        except UnicodeEncodeError:
 
158
            # cmd_annotate should be passing in an 'exact' object, which means
 
159
            # we have a direct handle to sys.stdout or equivalent. It may not
 
160
            # be able to handle the exact Unicode characters, but 'annotate' is
 
161
            # a user function (non-scripting), so shouldn't die because of
 
162
            # unrepresentable annotation characters. So encode using 'replace',
 
163
            # and write them again.
 
164
            to_file.write(anno.encode(encoding, 'replace'))
 
165
        to_file.write('| %s\n' % (text,))
 
166
        prevanno = anno
 
167
 
 
168
 
 
169
def _show_id_annotations(annotations, to_file, full):
 
170
    if not annotations:
 
171
        return
 
172
    last_rev_id = None
 
173
    max_origin_len = max(len(origin) for origin, text in annotations)
 
174
    for origin, text in annotations:
 
175
        if full or last_rev_id != origin:
 
176
            this = origin
 
177
        else:
 
178
            this = ''
 
179
        to_file.write('%*s | %s' % (max_origin_len, this, text))
 
180
        last_rev_id = origin
 
181
    return
 
182
 
 
183
 
 
184
def _expand_annotations(annotations, branch, current_rev=None):
 
185
    """Expand a file's annotations into command line UI ready tuples.
 
186
 
 
187
    Each tuple includes detailed information, such as the author name, and date
 
188
    string for the commit, rather than just the revision id.
 
189
 
 
190
    :param annotations: The annotations to expand.
 
191
    :param revision_id_to_revno: A map from id to revision numbers.
 
192
    :param branch: A locked branch to query for revision details.
91
193
    """
92
 
    branch_last_revision = branch.last_revision()
93
 
    revision_graph = branch.repository.get_revision_graph(branch_last_revision)
94
 
    merge_sorted_revisions = tsort.merge_sort(
95
 
        revision_graph,
96
 
        branch_last_revision,
97
 
        None,
98
 
        generate_revno=True)
99
 
    revision_id_to_revno = dict((rev_id, revno)
100
 
                                for seq_num, rev_id, depth, revno, end_of_merge
101
 
                                 in merge_sorted_revisions)
102
 
    w = branch.repository.weave_store.get_weave(file_id,
103
 
        branch.repository.get_transaction())
 
194
    repository = branch.repository
 
195
    if current_rev is not None:
 
196
        # This can probably become a function on MutableTree, get_revno_map
 
197
        # there, or something.
 
198
        last_revision = current_rev.revision_id
 
199
        # XXX: Partially Cloned from branch, uses the old_get_graph, eep.
 
200
        # XXX: The main difficulty is that we need to inject a single new node
 
201
        #      (current_rev) into the graph before it gets numbered, etc.
 
202
        #      Once KnownGraph gets an 'add_node()' function, we can use
 
203
        #      VF.get_known_graph_ancestry().
 
204
        graph = repository.get_graph()
 
205
        revision_graph = dict(((key, value) for key, value in
 
206
            graph.iter_ancestry(current_rev.parent_ids) if value is not None))
 
207
        revision_graph = _strip_NULL_ghosts(revision_graph)
 
208
        revision_graph[last_revision] = current_rev.parent_ids
 
209
        merge_sorted_revisions = tsort.merge_sort(
 
210
            revision_graph,
 
211
            last_revision,
 
212
            None,
 
213
            generate_revno=True)
 
214
        revision_id_to_revno = dict((rev_id, revno)
 
215
            for seq_num, rev_id, depth, revno, end_of_merge in
 
216
                merge_sorted_revisions)
 
217
    else:
 
218
        revision_id_to_revno = branch.get_revision_id_to_revno_map()
104
219
    last_origin = None
105
 
    annotations = list(w.annotate_iter(rev_id))
106
220
    revision_ids = set(o for o, t in annotations)
107
 
    revision_ids = [o for o in revision_ids if 
108
 
                    branch.repository.has_revision(o)]
109
 
    revisions = dict((r.revision_id, r) for r in 
110
 
                     branch.repository.get_revisions(revision_ids))
 
221
    revisions = {}
 
222
    if CURRENT_REVISION in revision_ids:
 
223
        revision_id_to_revno[CURRENT_REVISION] = (
 
224
            "%d?" % (branch.revno() + 1),)
 
225
        revisions[CURRENT_REVISION] = current_rev
 
226
    revision_ids = [o for o in revision_ids if
 
227
                    repository.has_revision(o)]
 
228
    revisions.update((r.revision_id, r) for r in
 
229
                     repository.get_revisions(revision_ids))
111
230
    for origin, text in annotations:
112
231
        text = text.rstrip('\r\n')
113
232
        if origin == last_origin:
125
244
                                     time.gmtime(rev.timestamp + tz))
126
245
            # a lazy way to get something like the email address
127
246
            # TODO: Get real email address
128
 
            author = rev.committer
 
247
            author = rev.get_apparent_authors()[0]
129
248
            try:
130
249
                author = extract_email_address(author)
131
250
            except errors.NoEmailInUsername:
133
252
        yield (revno_str, author, date_str, origin, text)
134
253
 
135
254
 
136
 
def reannotate(parents_lines, new_lines, new_revision_id):
 
255
def reannotate(parents_lines, new_lines, new_revision_id,
 
256
               _left_matching_blocks=None,
 
257
               heads_provider=None):
137
258
    """Create a new annotated version from new lines and parent annotations.
138
 
    
 
259
 
139
260
    :param parents_lines: List of annotated lines for all parents
140
261
    :param new_lines: The un-annotated new lines
141
262
    :param new_revision_id: The revision-id to associate with new lines
142
263
        (will often be CURRENT_REVISION)
 
264
    :param left_matching_blocks: a hint about which areas are common
 
265
        between the text and its left-hand-parent.  The format is
 
266
        the SequenceMatcher.get_matching_blocks format
 
267
        (start_left, start_right, length_of_match).
 
268
    :param heads_provider: An object which provides a .heads() call to resolve
 
269
        if any revision ids are children of others.
 
270
        If None, then any ancestry disputes will be resolved with
 
271
        new_revision_id
143
272
    """
144
 
    if len(parents_lines) == 1:
145
 
        for data in _reannotate(parents_lines[0], new_lines, new_revision_id):
146
 
            yield data
 
273
    if len(parents_lines) == 0:
 
274
        lines = [(new_revision_id, line) for line in new_lines]
 
275
    elif len(parents_lines) == 1:
 
276
        lines = _reannotate(parents_lines[0], new_lines, new_revision_id,
 
277
                            _left_matching_blocks)
 
278
    elif len(parents_lines) == 2:
 
279
        left = _reannotate(parents_lines[0], new_lines, new_revision_id,
 
280
                           _left_matching_blocks)
 
281
        lines = _reannotate_annotated(parents_lines[1], new_lines,
 
282
                                      new_revision_id, left,
 
283
                                      heads_provider)
147
284
    else:
148
 
        reannotations = [list(_reannotate(p, new_lines, new_revision_id)) for
149
 
                         p in parents_lines]
 
285
        reannotations = [_reannotate(parents_lines[0], new_lines,
 
286
                                     new_revision_id, _left_matching_blocks)]
 
287
        reannotations.extend(_reannotate(p, new_lines, new_revision_id)
 
288
                             for p in parents_lines[1:])
 
289
        lines = []
150
290
        for annos in zip(*reannotations):
151
291
            origins = set(a for a, l in annos)
152
 
            line = annos[0][1]
153
292
            if len(origins) == 1:
154
 
                yield iter(origins).next(), line
155
 
            elif len(origins) == 2 and new_revision_id in origins:
156
 
                yield (x for x in origins if x != new_revision_id).next(), line
 
293
                # All the parents agree, so just return the first one
 
294
                lines.append(annos[0])
157
295
            else:
158
 
                yield new_revision_id, line
159
 
 
160
 
 
161
 
def _reannotate(parent_lines, new_lines, new_revision_id):
162
 
    plain_parent_lines = [l for r, l in parent_lines]
163
 
    matcher = patiencediff.PatienceSequenceMatcher(None, plain_parent_lines,
164
 
                                                   new_lines)
 
296
                line = annos[0][1]
 
297
                if len(origins) == 2 and new_revision_id in origins:
 
298
                    origins.remove(new_revision_id)
 
299
                if len(origins) == 1:
 
300
                    lines.append((origins.pop(), line))
 
301
                else:
 
302
                    lines.append((new_revision_id, line))
 
303
    return lines
 
304
 
 
305
 
 
306
def _reannotate(parent_lines, new_lines, new_revision_id,
 
307
                matching_blocks=None):
165
308
    new_cur = 0
166
 
    for i, j, n in matcher.get_matching_blocks():
 
309
    if matching_blocks is None:
 
310
        plain_parent_lines = [l for r, l in parent_lines]
 
311
        matcher = patiencediff.PatienceSequenceMatcher(None,
 
312
            plain_parent_lines, new_lines)
 
313
        matching_blocks = matcher.get_matching_blocks()
 
314
    lines = []
 
315
    for i, j, n in matching_blocks:
167
316
        for line in new_lines[new_cur:j]:
168
 
            yield new_revision_id, line
169
 
        for data in parent_lines[i:i+n]:
170
 
            yield data
 
317
            lines.append((new_revision_id, line))
 
318
        lines.extend(parent_lines[i:i+n])
171
319
        new_cur = j + n
 
320
    return lines
 
321
 
 
322
 
 
323
def _get_matching_blocks(old, new):
 
324
    matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
 
325
    return matcher.get_matching_blocks()
 
326
 
 
327
 
 
328
_break_annotation_tie = None
 
329
 
 
330
def _old_break_annotation_tie(annotated_lines):
 
331
    """Chose an attribution between several possible ones.
 
332
 
 
333
    :param annotated_lines: A list of tuples ((file_id, rev_id), line) where
 
334
        the lines are identical but the revids different while no parent
 
335
        relation exist between them
 
336
 
 
337
     :return : The "winning" line. This must be one with a revid that
 
338
         guarantees that further criss-cross merges will converge. Failing to
 
339
         do so have performance implications.
 
340
    """
 
341
    # sort lexicographically so that we always get a stable result.
 
342
 
 
343
    # TODO: while 'sort' is the easiest (and nearly the only possible solution)
 
344
    # with the current implementation, chosing the oldest revision is known to
 
345
    # provide better results (as in matching user expectations). The most
 
346
    # common use case being manual cherry-pick from an already existing
 
347
    # revision.
 
348
    return sorted(annotated_lines)[0]
 
349
 
 
350
 
 
351
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
 
352
                                     child_lines, start_child, end_child,
 
353
                                     right_lines, start_right, end_right,
 
354
                                     heads_provider, revision_id):
 
355
    """Find lines in plain_right_lines that match the existing lines.
 
356
 
 
357
    :param output_lines: Append final annotated lines to this list
 
358
    :param plain_child_lines: The unannotated new lines for the child text
 
359
    :param child_lines: Lines for the child text which have been annotated
 
360
        for the left parent
 
361
 
 
362
    :param start_child: Position in plain_child_lines and child_lines to start
 
363
        the match searching
 
364
    :param end_child: Last position in plain_child_lines and child_lines to
 
365
        search for a match
 
366
    :param right_lines: The annotated lines for the whole text for the right
 
367
        parent
 
368
    :param start_right: Position in right_lines to start the match
 
369
    :param end_right: Last position in right_lines to search for a match
 
370
    :param heads_provider: When parents disagree on the lineage of a line, we
 
371
        need to check if one side supersedes the other
 
372
    :param revision_id: The label to give if a line should be labeled 'tip'
 
373
    """
 
374
    output_extend = output_lines.extend
 
375
    output_append = output_lines.append
 
376
    # We need to see if any of the unannotated lines match
 
377
    plain_right_subset = [l for a,l in right_lines[start_right:end_right]]
 
378
    plain_child_subset = plain_child_lines[start_child:end_child]
 
379
    match_blocks = _get_matching_blocks(plain_right_subset, plain_child_subset)
 
380
 
 
381
    last_child_idx = 0
 
382
 
 
383
    for right_idx, child_idx, match_len in match_blocks:
 
384
        # All the lines that don't match are just passed along
 
385
        if child_idx > last_child_idx:
 
386
            output_extend(child_lines[start_child + last_child_idx
 
387
                                      :start_child + child_idx])
 
388
        for offset in xrange(match_len):
 
389
            left = child_lines[start_child+child_idx+offset]
 
390
            right = right_lines[start_right+right_idx+offset]
 
391
            if left[0] == right[0]:
 
392
                # The annotations match, just return the left one
 
393
                output_append(left)
 
394
            elif left[0] == revision_id:
 
395
                # The left parent marked this as unmatched, so let the
 
396
                # right parent claim it
 
397
                output_append(right)
 
398
            else:
 
399
                # Left and Right both claim this line
 
400
                if heads_provider is None:
 
401
                    output_append((revision_id, left[1]))
 
402
                else:
 
403
                    heads = heads_provider.heads((left[0], right[0]))
 
404
                    if len(heads) == 1:
 
405
                        output_append((iter(heads).next(), left[1]))
 
406
                    else:
 
407
                        # Both claim different origins, get a stable result.
 
408
                        # If the result is not stable, there is a risk a
 
409
                        # performance degradation as criss-cross merges will
 
410
                        # flip-flop the attribution.
 
411
                        if _break_annotation_tie is None:
 
412
                            output_append(
 
413
                                _old_break_annotation_tie([left, right]))
 
414
                        else:
 
415
                            output_append(_break_annotation_tie([left, right]))
 
416
        last_child_idx = child_idx + match_len
 
417
 
 
418
 
 
419
def _reannotate_annotated(right_parent_lines, new_lines, new_revision_id,
 
420
                          annotated_lines, heads_provider):
 
421
    """Update the annotations for a node based on another parent.
 
422
 
 
423
    :param right_parent_lines: A list of annotated lines for the right-hand
 
424
        parent.
 
425
    :param new_lines: The unannotated new lines.
 
426
    :param new_revision_id: The revision_id to attribute to lines which are not
 
427
        present in either parent.
 
428
    :param annotated_lines: A list of annotated lines. This should be the
 
429
        annotation of new_lines based on parents seen so far.
 
430
    :param heads_provider: When parents disagree on the lineage of a line, we
 
431
        need to check if one side supersedes the other.
 
432
    """
 
433
    if len(new_lines) != len(annotated_lines):
 
434
        raise AssertionError("mismatched new_lines and annotated_lines")
 
435
    # First compare the newly annotated lines with the right annotated lines.
 
436
    # Lines which were not changed in left or right should match. This tends to
 
437
    # be the bulk of the lines, and they will need no further processing.
 
438
    lines = []
 
439
    lines_extend = lines.extend
 
440
    last_right_idx = 0 # The line just after the last match from the right side
 
441
    last_left_idx = 0
 
442
    matching_left_and_right = _get_matching_blocks(right_parent_lines,
 
443
                                                   annotated_lines)
 
444
    for right_idx, left_idx, match_len in matching_left_and_right:
 
445
        # annotated lines from last_left_idx to left_idx did not match the
 
446
        # lines from last_right_idx to right_idx, the raw lines should be
 
447
        # compared to determine what annotations need to be updated
 
448
        if last_right_idx == right_idx or last_left_idx == left_idx:
 
449
            # One of the sides is empty, so this is a pure insertion
 
450
            lines_extend(annotated_lines[last_left_idx:left_idx])
 
451
        else:
 
452
            # We need to see if any of the unannotated lines match
 
453
            _find_matching_unannotated_lines(lines,
 
454
                                             new_lines, annotated_lines,
 
455
                                             last_left_idx, left_idx,
 
456
                                             right_parent_lines,
 
457
                                             last_right_idx, right_idx,
 
458
                                             heads_provider,
 
459
                                             new_revision_id)
 
460
        last_right_idx = right_idx + match_len
 
461
        last_left_idx = left_idx + match_len
 
462
        # If left and right agree on a range, just push that into the output
 
463
        lines_extend(annotated_lines[left_idx:left_idx + match_len])
 
464
    return lines
 
465
 
 
466
 
 
467
try:
 
468
    from bzrlib._annotator_pyx import Annotator
 
469
except ImportError, e:
 
470
    osutils.failed_to_load_extension(e)
 
471
    from bzrlib._annotator_py import Annotator