/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/annotate.py

  • Committer: Jelmer Vernooij
  • Date: 2017-06-10 16:40:42 UTC
  • mfrom: (6653.6.7 rename-controldir)
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170610164042-zrxqgy2htyduvke2
MergeĀ rename-controldirĀ branch.

Show diffs side-by-side

added added

removed removed

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