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
encoding = osutils.get_terminal_encoding()
78
59
# Handle the show_ids case
79
annotations = list(tree.annotate_iter(path, file_id))
81
return _show_id_annotations(annotations, to_file, full, encoding)
83
if not getattr(tree, "get_revision_id", False):
84
# Create a virtual revision to represent the current tree state.
85
# Should get some more pending commit attributes, like pending tags,
87
current_rev = Revision(CURRENT_REVISION)
88
current_rev.parent_ids = tree.get_parent_ids()
90
current_rev.committer = branch.get_config_stack().get('email')
92
current_rev.committer = 'local user'
93
current_rev.message = "?"
94
current_rev.timestamp = round(time.time(), 3)
95
current_rev.timezone = osutils.local_time_offset()
98
annotation = list(_expand_annotations(annotations, branch,
100
_print_annotations(annotation, verbose, to_file, full, encoding)
103
def _print_annotations(annotation, verbose, to_file, full, encoding):
104
"""Print annotations to to_file.
106
:param to_file: The file to output the annotation to.
107
:param verbose: Show all details rather than truncating to ensure
108
reasonable text width.
109
: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))
111
75
if len(annotation) == 0:
112
76
max_origin_len = max_revno_len = max_revid_len = 0
130
96
anno = "%-*s %-7s " % (max_revno_len, revno_str, author[:7])
131
97
if anno.lstrip() == "" and full:
133
# GZ 2017-05-21: Writing both unicode annotation and bytes from file
134
# which the given to_file must cope with.
136
to_file.write('| %s\n' % (text.decode(encoding),))
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'))
109
to_file.write('| %s\n' % (text,))
140
def _show_id_annotations(annotations, to_file, full, encoding):
144
max_origin_len = max(len(origin) for origin, text in annotations)
145
for origin, text in annotations:
146
if full or last_rev_id != origin:
150
to_file.write('%*s | %s' % (max_origin_len, this.decode('utf-8'),
151
text.decode(encoding)))
156
def _expand_annotations(annotations, branch, current_rev=None):
157
"""Expand a file's annotations into command line UI ready tuples.
159
Each tuple includes detailed information, such as the author name, and date
160
string for the commit, rather than just the revision id.
162
:param annotations: The annotations to expand.
163
:param revision_id_to_revno: A map from id to revision numbers.
164
: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.
166
repository = branch.repository
126
revision_id_to_revno = branch.get_revision_id_to_revno_map()
127
annotations = _annotations(branch.repository, file_id, rev_id)
167
129
revision_ids = set(o for o, t in annotations)
168
if current_rev is not None:
169
# This can probably become a function on MutableTree, get_revno_map
170
# there, or something.
171
last_revision = current_rev.revision_id
172
# XXX: Partially Cloned from branch, uses the old_get_graph, eep.
173
# XXX: The main difficulty is that we need to inject a single new node
174
# (current_rev) into the graph before it gets numbered, etc.
175
# Once KnownGraph gets an 'add_node()' function, we can use
176
# VF.get_known_graph_ancestry().
177
graph = repository.get_graph()
178
revision_graph = dict(((key, value) for key, value in
179
graph.iter_ancestry(current_rev.parent_ids) if value is not None))
180
revision_graph = _strip_NULL_ghosts(revision_graph)
181
revision_graph[last_revision] = current_rev.parent_ids
182
merge_sorted_revisions = tsort.merge_sort(
187
revision_id_to_revno = dict((rev_id, revno)
188
for seq_num, rev_id, depth, revno, end_of_merge in
189
merge_sorted_revisions)
191
# TODO(jelmer): Only look up the revision ids that we need (i.e. those
192
# in revision_ids). Possibly add a HPSS call that can look those up
194
revision_id_to_revno = branch.get_revision_id_to_revno_map()
197
if CURRENT_REVISION in revision_ids:
198
revision_id_to_revno[CURRENT_REVISION] = (
199
"%d?" % (branch.revno() + 1),)
200
revisions[CURRENT_REVISION] = current_rev
203
repository.iter_revisions(revision_ids)
204
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))
205
134
for origin, text in annotations:
206
text = text.rstrip(b'\r\n')
135
text = text.rstrip('\r\n')
207
136
if origin == last_origin:
208
(revno_str, author, date_str) = ('', '', '')
137
(revno_str, author, date_str) = ('','','')
210
139
last_origin = origin
211
140
if origin not in revisions:
212
(revno_str, author, date_str) = ('?', '?', '?')
141
(revno_str, author, date_str) = ('?','?','?')
214
143
revno_str = '.'.join(str(i) for i in
215
144
revision_id_to_revno[origin])
298
227
def _get_matching_blocks(old, new):
299
matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
228
matcher = patiencediff.PatienceSequenceMatcher(None,
300
230
return matcher.get_matching_blocks()
303
_break_annotation_tie = None
305
def _old_break_annotation_tie(annotated_lines):
306
"""Chose an attribution between several possible ones.
308
:param annotated_lines: A list of tuples ((file_id, rev_id), line) where
309
the lines are identical but the revids different while no parent
310
relation exist between them
312
:return : The "winning" line. This must be one with a revid that
313
guarantees that further criss-cross merges will converge. Failing to
314
do so have performance implications.
316
# sort lexicographically so that we always get a stable result.
318
# TODO: while 'sort' is the easiest (and nearly the only possible solution)
319
# with the current implementation, chosing the oldest revision is known to
320
# provide better results (as in matching user expectations). The most
321
# common use case being manual cherry-pick from an already existing
323
return sorted(annotated_lines)[0]
326
233
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
327
234
child_lines, start_child, end_child,
328
235
right_lines, start_right, end_right,
378
284
heads = heads_provider.heads((left[0], right[0]))
379
285
if len(heads) == 1:
380
output_append((next(iter(heads)), left[1]))
286
output_append((iter(heads).next(), left[1]))
382
# Both claim different origins, get a stable result.
383
# If the result is not stable, there is a risk a
384
# performance degradation as criss-cross merges will
385
# flip-flop the attribution.
386
if _break_annotation_tie is None:
388
_old_break_annotation_tie([left, right]))
390
output_append(_break_annotation_tie([left, right]))
288
# Both claim different origins
289
output_append((revision_id, left[1]))
290
# We know that revision_id is the head for
291
# left and right, so cache it
292
heads_provider.cache(
293
(revision_id, left[0]),
295
heads_provider.cache(
296
(revision_id, right[0]),
391
298
last_child_idx = child_idx + match_len