107
def _enumerate_history(branch):
110
for rev_id in branch.revision_history():
111
rh.append((revno, rev_id))
69
116
def show_log(branch,
71
show_timezone='original',
118
specific_fileid=None,
75
124
"""Write out human-readable log of commits to this branch.
127
LogFormatter object to show the output.
78
130
If true, list only the commits affecting the specified
79
131
file, rather than all commits.
82
'original' (committer's timezone),
83
'utc' (universal time), or
84
'local' (local user's timezone)
87
134
If true show added/changed/deleted/renamed files.
90
If true, show revision and file ids.
93
File to send log to; by default stdout.
95
from osutils import format_date
96
from errors import BzrCheckError
97
from diff import compare_inventories
98
from textui import show_status
99
from inventory import Inventory
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
106
file_id = branch.read_working_inventory().path2id(filename)
108
for revno, revid, why in find_touching_revisions(branch, file_id):
112
for i, revid in enumerate(branch.revision_history()):
115
branch._need_readlock()
118
prev_inv = Inventory()
119
for revno, revision_id in which_revs():
120
print >>to_file, '-' * 60
121
print >>to_file, 'revno:', revno
122
rev = branch.get_revision(revision_id)
124
print >>to_file, 'revision-id:', revision_id
125
print >>to_file, 'committer:', rev.committer
126
print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
129
if revision_id != rev.revision_id:
130
raise BzrCheckError("retrieved wrong revision: %r"
131
% (revision_id, rev.revision_id))
133
print >>to_file, 'message:'
135
print >>to_file, ' (no message)'
137
for l in rev.message.split('\n'):
138
print >>to_file, ' ' + l
140
# Don't show a list of changed files if we were asked about
143
if verbose and not filename:
144
this_inv = branch.get_inventory(rev.inventory_id)
145
delta = compare_inventories(prev_inv, this_inv)
148
print >>to_file, 'removed files:'
149
for path, fid in delta.removed:
151
print >>to_file, ' %-30s %s' % (path, fid)
153
print >>to_file, ' ', path
155
print >>to_file, 'added files:'
156
for path, fid in delta.added:
158
print >>to_file, ' %-30s %s' % (path, fid)
160
print >>to_file, ' ' + path
162
print >>to_file, 'renamed files:'
163
for oldpath, newpath, fid in delta.renamed:
165
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
167
print >>to_file, ' %s => %s' % (oldpath, newpath)
169
print >>to_file, 'modified files:'
170
for path, fid in delta.modified:
172
print >>to_file, ' %-30s %s' % (path, fid)
174
print >>to_file, ' ' + path
178
precursor = revision_id
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),