33
from .lazy_import import lazy_import
34
lazy_import(globals(), """
47
from .repository import _strip_NULL_ghosts
48
from .revision import (
54
def annotate_file_tree(tree, path, to_file, verbose=False, full=False,
55
show_ids=False, branch=None):
56
"""Annotate path in a tree.
37
from bzrlib.config import extract_email_address
38
from bzrlib.repository import _strip_NULL_ghosts
39
from bzrlib.revision import CURRENT_REVISION, Revision
42
def annotate_file(branch, rev_id, file_id, verbose=False, full=False,
43
to_file=None, show_ids=False):
44
"""Annotate file_id at revision rev_id in branch.
46
The branch should already be read_locked() when annotate_file is called.
48
:param branch: The branch to look for revision numbers and history from.
49
:param rev_id: The revision id to annotate.
50
:param file_id: The file_id to annotate.
51
:param verbose: Show all details rather than truncating to ensure
52
reasonable text width.
53
:param full: XXXX Not sure what this does.
54
:param to_file: The file to output the annotation to; if None stdout is
56
:param show_ids: Show revision ids in the annotation output.
61
# Handle the show_ids case
62
annotations = _annotations(branch.repository, file_id, rev_id)
64
return _show_id_annotations(annotations, to_file, full)
66
# Calculate the lengths of the various columns
67
annotation = list(_expand_annotations(annotations, branch))
68
_print_annotations(annotation, verbose, to_file, full)
71
def annotate_file_tree(tree, file_id, to_file, verbose=False, full=False,
73
"""Annotate file_id in a tree.
58
75
The tree should already be read_locked() when annotate_file_tree is called.
60
77
:param tree: The tree to look for revision numbers and history from.
61
:param path: The path to annotate
78
:param file_id: The file_id to annotate.
62
79
:param to_file: The file to output the annotation to.
63
80
:param verbose: Show all details rather than truncating to ensure
64
81
reasonable text width.
65
82
:param full: XXXX Not sure what this does.
66
83
:param show_ids: Show revision ids in the annotation output.
67
:param branch: Branch to use for revision revno lookups
85
rev_id = tree.last_revision()
74
encoding = osutils.get_terminal_encoding()
75
88
# Handle the show_ids case
76
annotations = list(tree.annotate_iter(path))
89
annotations = list(tree.annotate_iter(file_id))
78
return _show_id_annotations(annotations, to_file, full, encoding)
80
if not getattr(tree, "get_revision_id", False):
81
# Create a virtual revision to represent the current tree state.
82
# Should get some more pending commit attributes, like pending tags,
84
current_rev = Revision(CURRENT_REVISION)
85
current_rev.parent_ids = tree.get_parent_ids()
87
current_rev.committer = branch.get_config_stack().get('email')
88
except errors.NoWhoami:
89
current_rev.committer = 'local user'
90
current_rev.message = "?"
91
current_rev.timestamp = round(time.time(), 3)
92
current_rev.timezone = osutils.local_time_offset()
95
annotation = list(_expand_annotations(
96
annotations, branch, current_rev))
97
_print_annotations(annotation, verbose, to_file, full, encoding)
100
def _print_annotations(annotation, verbose, to_file, full, encoding):
91
return _show_id_annotations(annotations, to_file, full)
93
# Create a virtual revision to represent the current tree state.
94
# Should get some more pending commit attributes, like pending tags,
96
current_rev = Revision(CURRENT_REVISION)
97
current_rev.parent_ids = tree.get_parent_ids()
98
current_rev.committer = tree.branch.get_config().username()
99
current_rev.message = "?"
100
current_rev.timestamp = round(time.time(), 3)
101
current_rev.timezone = osutils.local_time_offset()
102
annotation = list(_expand_annotations(annotations, tree.branch,
104
_print_annotations(annotation, verbose, to_file, full)
107
def _print_annotations(annotation, verbose, to_file, full):
101
108
"""Print annotations to to_file.
103
110
:param to_file: The file to output the annotation to.
106
113
:param full: XXXX Not sure what this does.
108
115
if len(annotation) == 0:
109
max_origin_len = max_revno_len = 0
116
max_origin_len = max_revno_len = max_revid_len = 0
111
118
max_origin_len = max(len(x[1]) for x in annotation)
112
119
max_revno_len = max(len(x[0]) for x in annotation)
120
max_revid_len = max(len(x[3]) for x in annotation)
114
122
max_revno_len = min(max_revno_len, 12)
115
123
max_revno_len = max(max_revno_len, 3)
117
125
# Output the annotations
127
encoding = getattr(to_file, 'encoding', None) or \
128
osutils.get_terminal_encoding()
119
129
for (revno_str, author, date_str, line_rev_id, text) in annotation:
121
131
anno = '%-*s %-*s %8s ' % (max_revno_len, revno_str,
122
132
max_origin_len, author, date_str)
124
134
if len(revno_str) > max_revno_len:
125
revno_str = revno_str[:max_revno_len - 1] + '>'
135
revno_str = revno_str[:max_revno_len-1] + '>'
126
136
anno = "%-*s %-7s " % (max_revno_len, revno_str, author[:7])
127
137
if anno.lstrip() == "" and full:
129
# GZ 2017-05-21: Writing both unicode annotation and bytes from file
130
# which the given to_file must cope with.
132
to_file.write('| %s\n' % (text.decode(encoding),))
141
except UnicodeEncodeError:
142
# cmd_annotate should be passing in an 'exact' object, which means
143
# we have a direct handle to sys.stdout or equivalent. It may not
144
# be able to handle the exact Unicode characters, but 'annotate' is
145
# a user function (non-scripting), so shouldn't die because of
146
# unrepresentable annotation characters. So encode using 'replace',
147
# and write them again.
148
to_file.write(anno.encode(encoding, 'replace'))
149
to_file.write('| %s\n' % (text,))
136
def _show_id_annotations(annotations, to_file, full, encoding):
153
def _show_id_annotations(annotations, to_file, full):
137
154
if not annotations:
139
156
last_rev_id = None
142
159
if full or last_rev_id != origin:
146
to_file.write('%*s | %s' % (
147
max_origin_len, this.decode('utf-8'), text.decode(encoding)))
163
to_file.write('%*s | %s' % (max_origin_len, this, text))
148
164
last_rev_id = origin
168
def _annotations(repo, file_id, rev_id):
169
"""Return the list of (origin_revision_id, line_text) for a revision of a file in a repository."""
170
annotations = repo.texts.annotate((file_id, rev_id))
172
return [(key[-1], line) for (key, line) in annotations]
152
175
def _expand_annotations(annotations, branch, current_rev=None):
153
"""Expand a file's annotations into command line UI ready tuples.
176
"""Expand a a files annotations into command line UI ready tuples.
155
178
Each tuple includes detailed information, such as the author name, and date
156
179
string for the commit, rather than just the revision id.
160
183
:param branch: A locked branch to query for revision details.
162
185
repository = branch.repository
163
revision_ids = set(o for o, t in annotations)
164
186
if current_rev is not None:
165
# This can probably become a function on MutableTree, get_revno_map
166
# there, or something.
187
# This can probably become a function on MutableTree, get_revno_map there,
167
189
last_revision = current_rev.revision_id
168
190
# XXX: Partially Cloned from branch, uses the old_get_graph, eep.
169
# XXX: The main difficulty is that we need to inject a single new node
170
# (current_rev) into the graph before it gets numbered, etc.
171
# Once KnownGraph gets an 'add_node()' function, we can use
172
# VF.get_known_graph_ancestry().
173
191
graph = repository.get_graph()
175
key: value for key, value in
176
graph.iter_ancestry(current_rev.parent_ids) if value is not None}
192
revision_graph = dict(((key, value) for key, value in
193
graph.iter_ancestry(current_rev.parent_ids) if value is not None))
177
194
revision_graph = _strip_NULL_ghosts(revision_graph)
178
195
revision_graph[last_revision] = current_rev.parent_ids
179
196
merge_sorted_revisions = tsort.merge_sort(
183
200
generate_revno=True)
184
revision_id_to_revno = {
201
revision_id_to_revno = dict((rev_id, revno)
186
202
for seq_num, rev_id, depth, revno, end_of_merge in
187
merge_sorted_revisions}
203
merge_sorted_revisions)
189
# TODO(jelmer): Only look up the revision ids that we need (i.e. those
190
# in revision_ids). Possibly add a HPSS call that can look those up
192
205
revision_id_to_revno = branch.get_revision_id_to_revno_map()
193
206
last_origin = None
207
revision_ids = set(o for o, t in annotations)
195
209
if CURRENT_REVISION in revision_ids:
196
210
revision_id_to_revno[CURRENT_REVISION] = (
197
211
"%d?" % (branch.revno() + 1),)
198
212
revisions[CURRENT_REVISION] = current_rev
201
repository.iter_revisions(revision_ids)
202
if entry[1] is not None)
213
revision_ids = [o for o in revision_ids if
214
repository.has_revision(o)]
215
revisions.update((r.revision_id, r) for r in
216
repository.get_revisions(revision_ids))
203
217
for origin, text in annotations:
204
text = text.rstrip(b'\r\n')
218
text = text.rstrip('\r\n')
205
219
if origin == last_origin:
206
(revno_str, author, date_str) = ('', '', '')
220
(revno_str, author, date_str) = ('','','')
208
222
last_origin = origin
209
223
if origin not in revisions:
210
(revno_str, author, date_str) = ('?', '?', '?')
224
(revno_str, author, date_str) = ('?','?','?')
212
revno_str = '.'.join(
213
str(i) for i in revision_id_to_revno[origin])
226
revno_str = '.'.join(str(i) for i in
227
revision_id_to_revno[origin])
214
228
rev = revisions[origin]
215
229
tz = rev.timezone or 0
216
230
date_str = time.strftime('%Y%m%d',
281
296
if matching_blocks is None:
282
297
plain_parent_lines = [l for r, l in parent_lines]
283
matcher = patiencediff.PatienceSequenceMatcher(
284
None, plain_parent_lines, new_lines)
298
matcher = patiencediff.PatienceSequenceMatcher(None,
299
plain_parent_lines, new_lines)
285
300
matching_blocks = matcher.get_matching_blocks()
287
302
for i, j, n in matching_blocks:
288
303
for line in new_lines[new_cur:j]:
289
304
lines.append((new_revision_id, line))
290
lines.extend(parent_lines[i:i + n])
305
lines.extend(parent_lines[i:i+n])
295
310
def _get_matching_blocks(old, new):
296
matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
311
matcher = patiencediff.PatienceSequenceMatcher(None,
297
313
return matcher.get_matching_blocks()
300
_break_annotation_tie = None
303
def _old_break_annotation_tie(annotated_lines):
304
"""Chose an attribution between several possible ones.
306
:param annotated_lines: A list of tuples ((file_id, rev_id), line) where
307
the lines are identical but the revids different while no parent
308
relation exist between them
310
:return : The "winning" line. This must be one with a revid that
311
guarantees that further criss-cross merges will converge. Failing to
312
do so have performance implications.
314
# sort lexicographically so that we always get a stable result.
316
# TODO: while 'sort' is the easiest (and nearly the only possible solution)
317
# with the current implementation, chosing the oldest revision is known to
318
# provide better results (as in matching user expectations). The most
319
# common use case being manual cherry-pick from an already existing
321
return sorted(annotated_lines)[0]
324
316
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
325
317
child_lines, start_child, end_child,
326
318
right_lines, start_right, end_right,
376
367
heads = heads_provider.heads((left[0], right[0]))
377
368
if len(heads) == 1:
378
output_append((next(iter(heads)), left[1]))
369
output_append((iter(heads).next(), left[1]))
380
# Both claim different origins, get a stable result.
381
# If the result is not stable, there is a risk a
382
# performance degradation as criss-cross merges will
383
# flip-flop the attribution.
384
if _break_annotation_tie is None:
386
_old_break_annotation_tie([left, right]))
388
output_append(_break_annotation_tie([left, right]))
371
# Both claim different origins, sort lexicographically
372
# so that we always get a stable result.
373
output_append(sorted([left, right])[0])
389
374
last_child_idx = child_idx + match_len
410
395
# be the bulk of the lines, and they will need no further processing.
412
397
lines_extend = lines.extend
413
# The line just after the last match from the right side
398
last_right_idx = 0 # The line just after the last match from the right side
415
399
last_left_idx = 0
416
400
matching_left_and_right = _get_matching_blocks(right_parent_lines,
418
402
for right_idx, left_idx, match_len in matching_left_and_right:
419
# annotated lines from last_left_idx to left_idx did not match the
420
# lines from last_right_idx to right_idx, the raw lines should be
421
# compared to determine what annotations need to be updated
403
# annotated lines from last_left_idx to left_idx did not match the lines from
405
# to right_idx, the raw lines should be compared to determine what annotations
422
407
if last_right_idx == right_idx or last_left_idx == left_idx:
423
408
# One of the sides is empty, so this is a pure insertion
424
409
lines_extend(annotated_lines[last_left_idx:left_idx])