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