172
191
# list indexes are 0-based; revisions are 1-based
173
192
cut_revs = which_revs[(start_revision-1):(end_revision)]
175
if direction == 'reverse':
177
elif direction == 'forward':
180
raise ValueError('invalid direction %r' % direction)
182
for revno, rev_id in cut_revs:
183
if verbose or specific_fileid:
184
delta = branch.get_revision_delta(revno)
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:]
187
if not delta.touches_file_id(specific_fileid):
191
# although we calculated it, throw it away without display
194
rev = branch.get_revision(rev_id)
235
# now we just print all the revisions
236
for ((rev_id, revno, merge_depth), (rev, delta)) in \
237
izip(view_revisions, iter_revisions()):
197
240
if not searchRE.search(rev.message):
200
lf.show(revno, rev, delta)
204
def deltas_for_log_dummy(branch, which_revs):
205
"""Return all the revisions without intermediate deltas.
207
Useful for log commands that won't need the delta information.
244
# a mainline revision.
247
if not delta.touches_file_id(specific_fileid):
210
for revno, revision_id in which_revs:
211
yield revno, branch.get_revision(revision_id), None
214
def deltas_for_log_reverse(branch, which_revs):
215
"""Compute deltas for display in latest-to-earliest order.
221
Sequence of (revno, revision_id) for the subset of history to examine
224
Sequence of (revno, rev, delta)
226
The delta is from the given revision to the next one in the
227
sequence, which makes sense if the log is being displayed from
230
last_revno = last_revision_id = last_tree = None
231
for revno, revision_id in which_revs:
232
this_tree = branch.revision_tree(revision_id)
233
this_revision = branch.get_revision(revision_id)
236
yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
238
this_tree = EmptyTree(branch.get_root_id())
241
last_revision = this_revision
242
last_tree = this_tree
246
this_tree = EmptyTree(branch.get_root_id())
248
this_revno = last_revno - 1
249
this_revision_id = branch.revision_history()[this_revno]
250
this_tree = branch.revision_tree(this_revision_id)
251
yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
254
def deltas_for_log_forward(branch, which_revs):
255
"""Compute deltas for display in forward log.
257
Given a sequence of (revno, revision_id) pairs, return
260
The delta is from the given revision to the next one in the
261
sequence, which makes sense if the log is being displayed from
264
last_revno = last_revision_id = last_tree = None
265
prev_tree = EmptyTree(branch.get_root_id())
267
for revno, revision_id in which_revs:
268
this_tree = branch.revision_tree(revision_id)
269
this_revision = branch.get_revision(revision_id)
273
last_tree = EmptyTree(branch.get_root_id())
275
last_revno = revno - 1
276
last_revision_id = branch.revision_history()[last_revno]
277
last_tree = branch.revision_tree(last_revision_id)
279
yield revno, this_revision, compare_trees(last_tree, this_tree, False)
282
last_revision = this_revision
283
last_tree = this_tree
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:
286
313
class LogFormatter(object):
287
314
"""Abstract class to display log messages."""
288
316
def __init__(self, to_file, show_ids=False, show_timezone='original'):
289
317
self.to_file = to_file
290
318
self.show_ids = show_ids
291
319
self.show_timezone = show_timezone
294
321
def show(self, revno, rev, delta):
295
322
raise NotImplementedError('not implemented in abstract base')
324
def short_committer(self, rev):
325
return re.sub('<.*@.*>', '', rev.committer).strip(' ')
302
328
class LongLogFormatter(LogFormatter):
303
329
def show(self, revno, rev, delta):
304
from osutils import format_date
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
306
338
to_file = self.to_file
308
print >>to_file, '-' * 60
309
print >>to_file, 'revno:', revno
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
310
346
if self.show_ids:
311
print >>to_file, 'revision-id:', rev.revision_id
313
347
for parent_id in rev.parent_ids:
314
print >>to_file, 'parent:', parent_id
316
print >>to_file, 'committer:', rev.committer
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']
318
355
date_str = format_date(rev.timestamp,
319
356
rev.timezone or 0,
320
357
self.show_timezone)
321
print >>to_file, 'timestamp: %s' % date_str
358
print >>to_file, indent+'timestamp: %s' % date_str
323
print >>to_file, 'message:'
360
print >>to_file, indent+'message:'
324
361
if not rev.message:
325
print >>to_file, ' (no message)'
362
print >>to_file, indent+' (no message)'
327
for l in rev.message.split('\n'):
328
print >>to_file, ' ' + l
364
message = rev.message.rstrip('\r\n')
365
for l in message.split('\n'):
366
print >>to_file, indent+' ' + l
330
367
if delta != None:
331
368
delta.show(to_file, self.show_ids)
335
371
class ShortLogFormatter(LogFormatter):
336
372
def show(self, revno, rev, delta):
337
373
from bzrlib.osutils import format_date
339
375
to_file = self.to_file
341
print >>to_file, "%5d %s\t%s" % (revno, rev.committer,
376
date_str = format_date(rev.timestamp, rev.timezone or 0,
378
print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
342
379
format_date(rev.timestamp, rev.timezone or 0,
380
self.show_timezone, date_fmt="%Y-%m-%d",
344
382
if self.show_ids:
345
383
print >>to_file, ' revision-id:', rev.revision_id
346
384
if not rev.message:
347
385
print >>to_file, ' (no message)'
349
for l in rev.message.split('\n'):
387
message = rev.message.rstrip('\r\n')
388
for l in message.split('\n'):
350
389
print >>to_file, ' ' + l
352
391
# TODO: Why not show the modified files in a shorter form as
353
392
# well? rewrap them single lines of appropriate length
354
393
if delta != None:
355
394
delta.show(to_file, self.show_ids)
360
FORMATTERS = {'long': LongLogFormatter,
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,
361
444
'short': ShortLogFormatter,
445
'line': LineLogFormatter,
448
def register_formatter(name, formatter):
449
FORMATTERS[name] = formatter
365
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.
366
457
from bzrlib.errors import BzrCommandError
369
459
return FORMATTERS[name](*args, **kwargs)
371
461
raise BzrCommandError("unknown log formatter: %r" % name)
373
463
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
374
# deprecated; for compatability
464
# deprecated; for compatibility
375
465
lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
376
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),