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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""File annotate based on weave storage"""
19
from __future__ import absolute_import
21
19
# TODO: Choice of more or less verbose formats:
23
21
# interposed: show more details between blocks of modified lines
25
23
# TODO: Show which revision caused a line to merge into the parent
27
25
# TODO: perhaps abbreviate timescales depending on how recent they are
28
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.
26
# e.g. "3:12 Tue", "13 Oct", "Oct 2005", etc.
33
from .lazy_import import lazy_import
34
lazy_import(globals(), """
47
extract_email_address,
49
from .repository import _strip_NULL_ghosts
50
from .revision import (
56
def annotate_file_tree(tree, path, to_file, verbose=False, full=False,
57
show_ids=False, branch=None, file_id=None):
58
"""Annotate file_id in a tree.
60
The tree should already be read_locked() when annotate_file_tree is called.
62
:param tree: The tree to look for revision numbers and history from.
63
:param path: The path to annotate
64
:param to_file: The file to output the annotation to.
37
from bzrlib.config import extract_email_address
40
def annotate_file(branch, rev_id, file_id, verbose=False, full=False,
41
to_file=None, show_ids=False):
42
"""Annotate file_id at revision rev_id in branch.
44
The branch should already be read_locked() when annotate_file is called.
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.
65
49
:param verbose: Show all details rather than truncating to ensure
66
50
reasonable text width.
67
51
:param full: XXXX Not sure what this does.
52
:param to_file: The file to output the annotation to; if None stdout is
68
54
:param show_ids: Show revision ids in the annotation output.
69
:param file_id: The file_id to annotate (must match file path)
70
:param branch: Branch to use for revision revno lookups
74
56
if to_file is None:
75
57
to_file = sys.stdout
77
59
# Handle the show_ids case
78
annotations = list(tree.annotate_iter(path, file_id))
80
return _show_id_annotations(annotations, to_file, full)
82
if not getattr(tree, "get_revision_id", False):
83
# Create a virtual revision to represent the current tree state.
84
# Should get some more pending commit attributes, like pending tags,
86
current_rev = Revision(CURRENT_REVISION)
87
current_rev.parent_ids = tree.get_parent_ids()
89
current_rev.committer = branch.get_config_stack().get('email')
91
current_rev.committer = 'local user'
92
current_rev.message = "?"
93
current_rev.timestamp = round(time.time(), 3)
94
current_rev.timezone = osutils.local_time_offset()
97
annotation = list(_expand_annotations(annotations, branch,
99
_print_annotations(annotation, verbose, to_file, full)
102
def _print_annotations(annotation, verbose, to_file, full):
103
"""Print annotations to to_file.
105
:param to_file: The file to output the annotation to.
106
:param verbose: Show all details rather than truncating to ensure
107
reasonable text width.
108
:param full: XXXX Not sure what this does.
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:
69
to_file.write('%*s | %s' % (max_origin_len, this, text))
73
# Calculate the lengths of the various columns
74
annotation = list(_annotate_file(branch, rev_id, file_id))
110
75
if len(annotation) == 0:
111
76
max_origin_len = max_revno_len = max_revid_len = 0
129
96
anno = "%-*s %-7s " % (max_revno_len, revno_str, author[:7])
130
97
if anno.lstrip() == "" and full:
132
# GZ 2017-05-21: Writing both unicode annotation and bytes from file
133
# which the given to_file must cope with.
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'))
135
109
to_file.write('| %s\n' % (text,))
139
def _show_id_annotations(annotations, to_file, full):
143
max_origin_len = max(len(origin) for origin, text in annotations)
144
for origin, text in annotations:
145
if full or last_rev_id != origin:
149
to_file.write('%*s | %s' % (max_origin_len, this, text))
154
def _expand_annotations(annotations, branch, current_rev=None):
155
"""Expand a file's annotations into command line UI ready tuples.
157
Each tuple includes detailed information, such as the author name, and date
158
string for the commit, rather than just the revision id.
160
:param annotations: The annotations to expand.
161
:param revision_id_to_revno: A map from id to revision numbers.
162
:param branch: A locked branch to query for revision details.
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))
117
return [(key[-1], line) for (key, line) in annotations]
120
def _annotate_file(branch, rev_id, file_id):
121
"""Yield the origins for each line of a file.
123
This includes detailed information, such as the author name, and
124
date string for the commit, rather than just the revision id.
164
repository = branch.repository
126
revision_id_to_revno = branch.get_revision_id_to_revno_map()
127
annotations = _annotations(branch.repository, file_id, rev_id)
165
129
revision_ids = set(o for o, t in annotations)
166
if current_rev is not None:
167
# This can probably become a function on MutableTree, get_revno_map
168
# there, or something.
169
last_revision = current_rev.revision_id
170
# XXX: Partially Cloned from branch, uses the old_get_graph, eep.
171
# XXX: The main difficulty is that we need to inject a single new node
172
# (current_rev) into the graph before it gets numbered, etc.
173
# Once KnownGraph gets an 'add_node()' function, we can use
174
# VF.get_known_graph_ancestry().
175
graph = repository.get_graph()
176
revision_graph = dict(((key, value) for key, value in
177
graph.iter_ancestry(current_rev.parent_ids) if value is not None))
178
revision_graph = _strip_NULL_ghosts(revision_graph)
179
revision_graph[last_revision] = current_rev.parent_ids
180
merge_sorted_revisions = tsort.merge_sort(
185
revision_id_to_revno = dict((rev_id, revno)
186
for seq_num, rev_id, depth, revno, end_of_merge in
187
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
revision_id_to_revno = branch.get_revision_id_to_revno_map()
195
if CURRENT_REVISION in revision_ids:
196
revision_id_to_revno[CURRENT_REVISION] = (
197
"%d?" % (branch.revno() + 1),)
198
revisions[CURRENT_REVISION] = current_rev
201
repository.iter_revisions(revision_ids)
202
if entry[1] is not None)
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))
203
134
for origin, text in annotations:
204
text = text.rstrip(b'\r\n')
135
text = text.rstrip('\r\n')
205
136
if origin == last_origin:
206
(revno_str, author, date_str) = ('', '', '')
137
(revno_str, author, date_str) = ('','','')
208
139
last_origin = origin
209
140
if origin not in revisions:
210
(revno_str, author, date_str) = ('?', '?', '?')
141
(revno_str, author, date_str) = ('?','?','?')
212
143
revno_str = '.'.join(str(i) for i in
213
144
revision_id_to_revno[origin])
296
227
def _get_matching_blocks(old, new):
297
matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
228
matcher = patiencediff.PatienceSequenceMatcher(None,
298
230
return matcher.get_matching_blocks()
301
_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
233
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
325
234
child_lines, start_child, end_child,
326
235
right_lines, start_right, end_right,
376
284
heads = heads_provider.heads((left[0], right[0]))
377
285
if len(heads) == 1:
378
output_append((next(iter(heads)), left[1]))
286
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]))
288
# Both claim different origins, sort lexicographically
289
# so that we always get a stable result.
290
output_append(sorted([left, right])[0])
389
291
last_child_idx = child_idx + match_len