1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
19
"""Code to show logs of changes.
21
Various flavors of log can be produced:
23
* for one file, or the whole tree, and (not done yet) for
24
files in a given directory
26
* in "verbose" mode with a description of what changed from one
29
* with file-ids and revision-ids shown
31
Logs are actually written out through an abstract LogFormatter
32
interface, which allows for different preferred formats. Plugins can
35
Logs can be produced in either forward (oldest->newest) or reverse
36
(newest->oldest) order.
38
Logs can be filtered to show only revisions matching a particular
39
search string, or within a particular range of revisions. The range
40
can be given as date/times, which are reduced to revisions before
43
In verbose mode we show a summary of what changed in each particular
44
revision. Note that this is the delta for changes in that revision
45
relative to its mainline parent, not the delta relative to the last
46
logged revision. So for example if you ask for a verbose log of
47
changes touching hello.c you will get a list of those revisions also
48
listing other things that were changed in the same revision, but not
49
all the changes since the previous revision that touched hello.c.
52
# TODO: option to show delta summaries for merged-in revisions
54
from itertools import izip
57
import bzrlib.errors as errors
58
from bzrlib.trace import mutter
59
from bzrlib.tsort import merge_sort
62
def find_touching_revisions(branch, file_id):
63
"""Yield a description of revisions which affect the file_id.
65
Each returned element is (revno, revision_id, description)
67
This is the list of revisions where the file is either added,
68
modified, renamed or deleted.
70
TODO: Perhaps some way to limit this to only particular revisions,
71
or to traverse a non-mainline set of revisions?
76
for revision_id in branch.revision_history():
77
this_inv = branch.repository.get_revision_inventory(revision_id)
78
if file_id in this_inv:
79
this_ie = this_inv[file_id]
80
this_path = this_inv.id2path(file_id)
82
this_ie = this_path = None
84
# now we know how it was last time, and how it is in this revision.
85
# are those two states effectively the same or not?
87
if not this_ie and not last_ie:
88
# not present in either
90
elif this_ie and not last_ie:
91
yield revno, revision_id, "added " + this_path
92
elif not this_ie and last_ie:
94
yield revno, revision_id, "deleted " + last_path
95
elif this_path != last_path:
96
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
97
elif (this_ie.text_size != last_ie.text_size
98
or this_ie.text_sha1 != last_ie.text_sha1):
99
yield revno, revision_id, "modified " + this_path
102
last_path = this_path
107
def _enumerate_history(branch):
110
for rev_id in branch.revision_history():
111
rh.append((revno, rev_id))
118
specific_fileid=None,
124
"""Write out human-readable log of commits to this branch.
127
LogFormatter object to show the output.
130
If true, list only the commits affecting the specified
131
file, rather than all commits.
134
If true show added/changed/deleted/renamed files.
137
'reverse' (default) is latest to earliest;
138
'forward' is earliest to latest.
141
If not None, only show revisions >= start_revision
144
If not None, only show revisions <= end_revision
148
_show_log(branch, lf, specific_fileid, verbose, direction,
149
start_revision, end_revision, search)
153
def _show_log(branch,
155
specific_fileid=None,
161
"""Worker function for show_log - see show_log."""
162
from bzrlib.osutils import format_date
163
from bzrlib.errors import BzrCheckError
165
from warnings import warn
167
if not isinstance(lf, LogFormatter):
168
warn("not a LogFormatter instance: %r" % lf)
171
mutter('get log for file_id %r', specific_fileid)
173
if search is not None:
175
searchRE = re.compile(search, re.IGNORECASE)
179
which_revs = _enumerate_history(branch)
181
if start_revision is None:
184
branch.check_real_revno(start_revision)
186
if end_revision is None:
187
end_revision = len(which_revs)
189
branch.check_real_revno(end_revision)
191
# list indexes are 0-based; revisions are 1-based
192
cut_revs = which_revs[(start_revision-1):(end_revision)]
196
# convert the revision history to a dictionary:
197
rev_nos = dict((k, v) for v, k in cut_revs)
199
# override the mainline to look like the revision history.
200
mainline_revs = [revision_id for index, revision_id in cut_revs]
201
if cut_revs[0][0] == 1:
202
mainline_revs.insert(0, None)
204
mainline_revs.insert(0, which_revs[start_revision-2][1])
205
if getattr(lf, 'show_merge', None) is not None:
206
include_merges = True
208
include_merges = False
209
view_revisions = list(get_view_revisions(mainline_revs, rev_nos, branch,
210
direction, include_merges=include_merges))
212
def iter_revisions():
213
# r = revision, n = revno, d = merge depth
214
revision_ids = [r for r, n, d in view_revisions]
215
zeros = set(r for r, n, d in view_revisions if d == 0)
217
repository = branch.repository
220
revisions = repository.get_revisions(revision_ids[:num])
221
if verbose or specific_fileid:
222
delta_revisions = [r for r in revisions if
223
r.revision_id in zeros]
224
deltas = repository.get_deltas_for_revisions(delta_revisions)
225
cur_deltas = dict(izip((r.revision_id for r in
226
delta_revisions), deltas))
227
for revision in revisions:
228
# The delta value will be None unless
229
# 1. verbose or specific_fileid is specified, and
230
# 2. the revision is a mainline revision
231
yield revision, cur_deltas.get(revision.revision_id)
232
revision_ids = revision_ids[num:]
235
# now we just print all the revisions
236
for ((rev_id, revno, merge_depth), (rev, delta)) in \
237
izip(view_revisions, iter_revisions()):
240
if not searchRE.search(rev.message):
244
# a mainline revision.
247
if not delta.touches_file_id(specific_fileid):
251
# although we calculated it, throw it away without display
254
lf.show(revno, rev, delta)
256
lf.show_merge(rev, merge_depth)
259
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
260
include_merges=True):
261
"""Produce an iterator of revisions to show
262
:return: an iterator of (revision_id, revno, merge_depth)
263
(if there is no revno for a revision, None is supplied)
265
if include_merges is False:
266
revision_ids = mainline_revs[1:]
267
if direction == 'reverse':
268
revision_ids.reverse()
269
for revision_id in revision_ids:
270
yield revision_id, rev_nos[revision_id], 0
272
merge_sorted_revisions = merge_sort(
273
branch.repository.get_revision_graph(mainline_revs[-1]),
277
if direction == 'forward':
278
# forward means oldest first.
279
merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
280
elif direction != 'reverse':
281
raise ValueError('invalid direction %r' % direction)
283
revision_history = branch.revision_history()
285
for sequence, rev_id, merge_depth, end_of_merge in merge_sorted_revisions:
286
yield rev_id, rev_nos.get(rev_id), merge_depth
289
def reverse_by_depth(merge_sorted_revisions, _depth=0):
290
"""Reverse revisions by depth.
292
Revisions with a different depth are sorted as a group with the previous
293
revision of that depth. There may be no topological justification for this,
294
but it looks much nicer.
297
for val in merge_sorted_revisions:
299
zd_revisions.append([val])
301
assert val[2] > _depth
302
zd_revisions[-1].append(val)
303
for revisions in zd_revisions:
304
if len(revisions) > 1:
305
revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
306
zd_revisions.reverse()
308
for chunk in zd_revisions:
313
class LogFormatter(object):
314
"""Abstract class to display log messages."""
316
def __init__(self, to_file, show_ids=False, show_timezone='original'):
317
self.to_file = to_file
318
self.show_ids = show_ids
319
self.show_timezone = show_timezone
321
def show(self, revno, rev, delta):
322
raise NotImplementedError('not implemented in abstract base')
324
def short_committer(self, rev):
325
return re.sub('<.*@.*>', '', rev.committer).strip(' ')
328
class LongLogFormatter(LogFormatter):
329
def show(self, revno, rev, delta):
330
return self._show_helper(revno=revno, rev=rev, delta=delta)
332
def show_merge(self, rev, merge_depth):
333
return self._show_helper(rev=rev, indent=' '*merge_depth, merged=True, delta=None)
335
def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
336
"""Show a revision, either merged or not."""
337
from bzrlib.osutils import format_date
338
to_file = self.to_file
339
print >>to_file, indent+'-' * 60
340
if revno is not None:
341
print >>to_file, 'revno:', revno
343
print >>to_file, indent+'merged:', rev.revision_id
345
print >>to_file, indent+'revision-id:', rev.revision_id
347
for parent_id in rev.parent_ids:
348
print >>to_file, indent+'parent:', parent_id
349
print >>to_file, indent+'committer:', rev.committer
351
print >>to_file, indent+'branch nick: %s' % \
352
rev.properties['branch-nick']
355
date_str = format_date(rev.timestamp,
358
print >>to_file, indent+'timestamp: %s' % date_str
360
print >>to_file, indent+'message:'
362
print >>to_file, indent+' (no message)'
364
message = rev.message.rstrip('\r\n')
365
for l in message.split('\n'):
366
print >>to_file, indent+' ' + l
368
delta.show(to_file, self.show_ids)
371
class ShortLogFormatter(LogFormatter):
372
def show(self, revno, rev, delta):
373
from bzrlib.osutils import format_date
375
to_file = self.to_file
376
date_str = format_date(rev.timestamp, rev.timezone or 0,
378
print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
379
format_date(rev.timestamp, rev.timezone or 0,
380
self.show_timezone, date_fmt="%Y-%m-%d",
383
print >>to_file, ' revision-id:', rev.revision_id
385
print >>to_file, ' (no message)'
387
message = rev.message.rstrip('\r\n')
388
for l in message.split('\n'):
389
print >>to_file, ' ' + l
391
# TODO: Why not show the modified files in a shorter form as
392
# well? rewrap them single lines of appropriate length
394
delta.show(to_file, self.show_ids)
398
class LineLogFormatter(LogFormatter):
399
def truncate(self, str, max_len):
400
if len(str) <= max_len:
402
return str[:max_len-3]+'...'
404
def date_string(self, rev):
405
from bzrlib.osutils import format_date
406
return format_date(rev.timestamp, rev.timezone or 0,
407
self.show_timezone, date_fmt="%Y-%m-%d",
410
def message(self, rev):
412
return '(no message)'
416
def show(self, revno, rev, delta):
417
from bzrlib.osutils import terminal_width
418
print >> self.to_file, self.log_string(revno, rev, terminal_width()-1)
420
def log_string(self, revno, rev, max_chars):
421
"""Format log info into one string. Truncate tail of string
422
:param revno: revision number (int) or None.
423
Revision numbers counts from 1.
424
:param rev: revision info object
425
:param max_chars: maximum length of resulting string
426
:return: formatted truncated string
430
# show revno only when is not None
431
out.append("%d:" % revno)
432
out.append(self.truncate(self.short_committer(rev), 20))
433
out.append(self.date_string(rev))
434
out.append(rev.get_summary())
435
return self.truncate(" ".join(out).rstrip('\n'), max_chars)
438
def line_log(rev, max_chars):
439
lf = LineLogFormatter(None)
440
return lf.log_string(None, rev, max_chars)
443
'long': LongLogFormatter,
444
'short': ShortLogFormatter,
445
'line': LineLogFormatter,
448
def register_formatter(name, formatter):
449
FORMATTERS[name] = formatter
451
def log_formatter(name, *args, **kwargs):
452
"""Construct a formatter from arguments.
454
name -- Name of the formatter to construct; currently 'long', 'short' and
455
'line' are supported.
457
from bzrlib.errors import BzrCommandError
459
return FORMATTERS[name](*args, **kwargs)
461
raise BzrCommandError("unknown log formatter: %r" % name)
463
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
464
# deprecated; for compatibility
465
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
466
lf.show(revno, rev, delta)
468
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
469
"""Show the change in revision history comparing the old revision history to the new one.
471
:param branch: The branch where the revisions exist
472
:param old_rh: The old revision history
473
:param new_rh: The new revision history
474
:param to_file: A file to write the results to. If None, stdout will be used
480
to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
481
lf = log_formatter(log_format,
484
show_timezone='original')
486
# This is the first index which is different between
489
for i in xrange(max(len(new_rh),
493
or new_rh[i] != old_rh[i]):
498
to_file.write('Nothing seems to have changed\n')
500
## TODO: It might be nice to do something like show_log
501
## and show the merged entries. But since this is the
502
## removed revisions, it shouldn't be as important
503
if base_idx < len(old_rh):
504
to_file.write('*'*60)
505
to_file.write('\nRemoved Revisions:\n')
506
for i in range(base_idx, len(old_rh)):
507
rev = branch.repository.get_revision(old_rh[i])
508
lf.show(i+1, rev, None)
509
to_file.write('*'*60)
510
to_file.write('\n\n')
511
if base_idx < len(new_rh):
512
to_file.write('Added Revisions:\n')
518
start_revision=base_idx+1,
519
end_revision=len(new_rh),