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
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
31
from bzrlib import (
42
37
from bzrlib.config import extract_email_address
43
from bzrlib.repository import _strip_NULL_ghosts
44
from bzrlib.revision import CURRENT_REVISION, Revision
47
40
def annotate_file(branch, rev_id, file_id, verbose=False, full=False,
64
57
to_file = sys.stdout
66
59
# Handle the show_ids case
67
annotations = _annotations(branch.repository, file_id, rev_id)
69
return _show_id_annotations(annotations, to_file, full)
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))
71
73
# Calculate the lengths of the various columns
72
annotation = list(_expand_annotations(annotations, branch))
73
_print_annotations(annotation, verbose, to_file, full)
76
def annotate_file_tree(tree, file_id, to_file, verbose=False, full=False,
78
"""Annotate file_id in a tree.
80
The tree should already be read_locked() when annotate_file_tree is called.
82
:param tree: The tree to look for revision numbers and history from.
83
:param file_id: The file_id to annotate.
84
:param to_file: The file to output the annotation to.
85
:param verbose: Show all details rather than truncating to ensure
86
reasonable text width.
87
:param full: XXXX Not sure what this does.
88
:param show_ids: Show revision ids in the annotation output.
90
rev_id = tree.last_revision()
93
# Handle the show_ids case
94
annotations = list(tree.annotate_iter(file_id))
96
return _show_id_annotations(annotations, to_file, full)
98
# Create a virtual revision to represent the current tree state.
99
# Should get some more pending commit attributes, like pending tags,
101
current_rev = Revision(CURRENT_REVISION)
102
current_rev.parent_ids = tree.get_parent_ids()
103
current_rev.committer = tree.branch.get_config().username()
104
current_rev.message = "?"
105
current_rev.timestamp = round(time.time(), 3)
106
current_rev.timezone = osutils.local_time_offset()
107
annotation = list(_expand_annotations(annotations, tree.branch,
109
_print_annotations(annotation, verbose, to_file, full)
112
def _print_annotations(annotation, verbose, to_file, full):
113
"""Print annotations to to_file.
115
:param to_file: The file to output the annotation to.
116
:param verbose: Show all details rather than truncating to ensure
117
reasonable text width.
118
:param full: XXXX Not sure what this does.
74
annotation = list(_annotate_file(branch, rev_id, file_id))
120
75
if len(annotation) == 0:
121
76
max_origin_len = max_revno_len = max_revid_len = 0
158
def _show_id_annotations(annotations, to_file, full):
162
max_origin_len = max(len(origin) for origin, text in annotations)
163
for origin, text in annotations:
164
if full or last_rev_id != origin:
168
to_file.write('%*s | %s' % (max_origin_len, this, text))
173
113
def _annotations(repo, file_id, rev_id):
174
"""Return the list of (origin_revision_id, line_text) for a revision of a file in a repository."""
175
annotations = repo.texts.annotate((file_id, rev_id))
177
return [(key[-1], line) for (key, line) in annotations]
180
def _expand_annotations(annotations, branch, current_rev=None):
181
"""Expand a file's annotations into command line UI ready tuples.
183
Each tuple includes detailed information, such as the author name, and date
184
string for the commit, rather than just the revision id.
186
:param annotations: The annotations to expand.
187
:param revision_id_to_revno: A map from id to revision numbers.
188
:param branch: A locked branch to query for revision details.
114
"""Return the list of (origin,text) for a revision of a file in a repository."""
115
w = repo.weave_store.get_weave(file_id, repo.get_transaction())
116
return list(w.annotate_iter(rev_id))
119
def _annotate_file(branch, rev_id, file_id):
120
"""Yield the origins for each line of a file.
122
This includes detailed information, such as the author name, and
123
date string for the commit, rather than just the revision id.
190
repository = branch.repository
191
if current_rev is not None:
192
# This can probably become a function on MutableTree, get_revno_map there,
194
last_revision = current_rev.revision_id
195
# XXX: Partially Cloned from branch, uses the old_get_graph, eep.
196
# XXX: The main difficulty is that we need to inject a single new node
197
# (current_rev) into the graph before it gets numbered, etc.
198
# Once KnownGraph gets an 'add_node()' function, we can use
199
# VF.get_known_graph_ancestry().
200
graph = repository.get_graph()
201
revision_graph = dict(((key, value) for key, value in
202
graph.iter_ancestry(current_rev.parent_ids) if value is not None))
203
revision_graph = _strip_NULL_ghosts(revision_graph)
204
revision_graph[last_revision] = current_rev.parent_ids
205
merge_sorted_revisions = tsort.merge_sort(
210
revision_id_to_revno = dict((rev_id, revno)
211
for seq_num, rev_id, depth, revno, end_of_merge in
212
merge_sorted_revisions)
214
revision_id_to_revno = branch.get_revision_id_to_revno_map()
125
revision_id_to_revno = branch.get_revision_id_to_revno_map()
126
annotations = _annotations(branch.repository, file_id, rev_id)
215
127
last_origin = None
216
128
revision_ids = set(o for o, t in annotations)
218
if CURRENT_REVISION in revision_ids:
219
revision_id_to_revno[CURRENT_REVISION] = (
220
"%d?" % (branch.revno() + 1),)
221
revisions[CURRENT_REVISION] = current_rev
222
revision_ids = [o for o in revision_ids if
223
repository.has_revision(o)]
224
revisions.update((r.revision_id, r) for r in
225
repository.get_revisions(revision_ids))
129
revision_ids = [o for o in revision_ids if
130
branch.repository.has_revision(o)]
131
revisions = dict((r.revision_id, r) for r in
132
branch.repository.get_revisions(revision_ids))
226
133
for origin, text in annotations:
227
134
text = text.rstrip('\r\n')
228
135
if origin == last_origin:
319
226
def _get_matching_blocks(old, new):
320
matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
227
matcher = patiencediff.PatienceSequenceMatcher(None,
321
229
return matcher.get_matching_blocks()
324
_break_annotation_tie = None
326
def _old_break_annotation_tie(annotated_lines):
327
"""Chose an attribution between several possible ones.
329
:param annotated_lines: A list of tuples ((file_id, rev_id), line) where
330
the lines are identical but the revids different while no parent
331
relation exist between them
333
:return : The "winning" line. This must be one with a revid that
334
guarantees that further criss-cross merges will converge. Failing to
335
do so have performance implications.
337
# sort lexicographically so that we always get a stable result.
339
# TODO: while 'sort' is the easiest (and nearly the only possible solution)
340
# with the current implementation, chosing the oldest revision is known to
341
# provide better results (as in matching user expectations). The most
342
# common use case being manual cherry-pick from an already existing
344
return sorted(annotated_lines)[0]
347
232
def _find_matching_unannotated_lines(output_lines, plain_child_lines,
348
233
child_lines, start_child, end_child,
349
234
right_lines, start_right, end_right,
400
284
if len(heads) == 1:
401
285
output_append((iter(heads).next(), left[1]))
403
# Both claim different origins, get a stable result.
404
# If the result is not stable, there is a risk a
405
# performance degradation as criss-cross merges will
406
# flip-flop the attribution.
407
if _break_annotation_tie is None:
409
_old_break_annotation_tie([left, right]))
411
output_append(_break_annotation_tie([left, right]))
287
# Both claim different origins
288
output_append((revision_id, left[1]))
289
# We know that revision_id is the head for
290
# left and right, so cache it
291
heads_provider.cache(
292
(revision_id, left[0]),
294
heads_provider.cache(
295
(revision_id, right[0]),
412
297
last_child_idx = child_idx + match_len
438
322
matching_left_and_right = _get_matching_blocks(right_parent_lines,
440
324
for right_idx, left_idx, match_len in matching_left_and_right:
441
# annotated lines from last_left_idx to left_idx did not match the
442
# lines from last_right_idx to right_idx, the raw lines should be
443
# compared to determine what annotations need to be updated
325
# annotated lines from last_left_idx to left_idx did not match the lines from
327
# to right_idx, the raw lines should be compared to determine what annotations
444
329
if last_right_idx == right_idx or last_left_idx == left_idx:
445
330
# One of the sides is empty, so this is a pure insertion
446
331
lines_extend(annotated_lines[last_left_idx:left_idx])