/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Martin Pool
  • Date: 2006-06-20 03:57:11 UTC
  • mto: This revision was merged to the branch mainline in revision 1798.
  • Revision ID: mbp@sourcefrog.net-20060620035711-400bb6b6bc6ff95b
Add pyflakes makefile target; fix many warnings

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
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.
 
7
 
 
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.
 
12
 
 
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
 
16
 
 
17
 
 
18
 
 
19
"""Code to show logs of changes.
 
20
 
 
21
Various flavors of log can be produced:
 
22
 
 
23
* for one file, or the whole tree, and (not done yet) for
 
24
  files in a given directory
 
25
 
 
26
* in "verbose" mode with a description of what changed from one
 
27
  version to the next
 
28
 
 
29
* with file-ids and revision-ids shown
 
30
 
 
31
Logs are actually written out through an abstract LogFormatter
 
32
interface, which allows for different preferred formats.  Plugins can
 
33
register formats too.
 
34
 
 
35
Logs can be produced in either forward (oldest->newest) or reverse
 
36
(newest->oldest) order.
 
37
 
 
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
 
41
calling in here.
 
42
 
 
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.
 
50
"""
 
51
 
 
52
 
 
53
# TODO: option to show delta summaries for merged-in revisions
 
54
#
 
55
# TODO: deltas_for_log_reverse seems no longer called; delete it?
 
56
 
 
57
import re
 
58
 
 
59
from bzrlib.delta import compare_trees
 
60
import bzrlib.errors as errors
 
61
from bzrlib.trace import mutter
 
62
from bzrlib.tree import EmptyTree
 
63
from bzrlib.tsort import merge_sort
 
64
 
 
65
 
 
66
def find_touching_revisions(branch, file_id):
 
67
    """Yield a description of revisions which affect the file_id.
 
68
 
 
69
    Each returned element is (revno, revision_id, description)
 
70
 
 
71
    This is the list of revisions where the file is either added,
 
72
    modified, renamed or deleted.
 
73
 
 
74
    TODO: Perhaps some way to limit this to only particular revisions,
 
75
    or to traverse a non-mainline set of revisions?
 
76
    """
 
77
    last_ie = None
 
78
    last_path = None
 
79
    revno = 1
 
80
    for revision_id in branch.revision_history():
 
81
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
82
        if file_id in this_inv:
 
83
            this_ie = this_inv[file_id]
 
84
            this_path = this_inv.id2path(file_id)
 
85
        else:
 
86
            this_ie = this_path = None
 
87
 
 
88
        # now we know how it was last time, and how it is in this revision.
 
89
        # are those two states effectively the same or not?
 
90
 
 
91
        if not this_ie and not last_ie:
 
92
            # not present in either
 
93
            pass
 
94
        elif this_ie and not last_ie:
 
95
            yield revno, revision_id, "added " + this_path
 
96
        elif not this_ie and last_ie:
 
97
            # deleted here
 
98
            yield revno, revision_id, "deleted " + last_path
 
99
        elif this_path != last_path:
 
100
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
 
101
        elif (this_ie.text_size != last_ie.text_size
 
102
              or this_ie.text_sha1 != last_ie.text_sha1):
 
103
            yield revno, revision_id, "modified " + this_path
 
104
 
 
105
        last_ie = this_ie
 
106
        last_path = this_path
 
107
        revno += 1
 
108
 
 
109
 
 
110
 
 
111
def _enumerate_history(branch):
 
112
    rh = []
 
113
    revno = 1
 
114
    for rev_id in branch.revision_history():
 
115
        rh.append((revno, rev_id))
 
116
        revno += 1
 
117
    return rh
 
118
 
 
119
 
 
120
def _get_revision_delta(branch, revno):
 
121
    """Return the delta for a mainline revision.
 
122
    
 
123
    This is used to show summaries in verbose logs, and also for finding 
 
124
    revisions which touch a given file."""
 
125
    # XXX: What are we supposed to do when showing a summary for something 
 
126
    # other than a mainline revision.  The delta to it's first parent, or
 
127
    # (more useful) the delta to a nominated other revision.
 
128
    return branch.get_revision_delta(revno)
 
129
 
 
130
 
 
131
def show_log(branch,
 
132
             lf,
 
133
             specific_fileid=None,
 
134
             verbose=False,
 
135
             direction='reverse',
 
136
             start_revision=None,
 
137
             end_revision=None,
 
138
             search=None):
 
139
    """Write out human-readable log of commits to this branch.
 
140
 
 
141
    lf
 
142
        LogFormatter object to show the output.
 
143
 
 
144
    specific_fileid
 
145
        If true, list only the commits affecting the specified
 
146
        file, rather than all commits.
 
147
 
 
148
    verbose
 
149
        If true show added/changed/deleted/renamed files.
 
150
 
 
151
    direction
 
152
        'reverse' (default) is latest to earliest;
 
153
        'forward' is earliest to latest.
 
154
 
 
155
    start_revision
 
156
        If not None, only show revisions >= start_revision
 
157
 
 
158
    end_revision
 
159
        If not None, only show revisions <= end_revision
 
160
    """
 
161
    branch.lock_read()
 
162
    try:
 
163
        _show_log(branch, lf, specific_fileid, verbose, direction,
 
164
                  start_revision, end_revision, search)
 
165
    finally:
 
166
        branch.unlock()
 
167
    
 
168
def _show_log(branch,
 
169
             lf,
 
170
             specific_fileid=None,
 
171
             verbose=False,
 
172
             direction='reverse',
 
173
             start_revision=None,
 
174
             end_revision=None,
 
175
             search=None):
 
176
    """Worker function for show_log - see show_log."""
 
177
    from bzrlib.osutils import format_date
 
178
    from bzrlib.errors import BzrCheckError
 
179
    from bzrlib.textui import show_status
 
180
    
 
181
    from warnings import warn
 
182
 
 
183
    if not isinstance(lf, LogFormatter):
 
184
        warn("not a LogFormatter instance: %r" % lf)
 
185
 
 
186
    if specific_fileid:
 
187
        mutter('get log for file_id %r', specific_fileid)
 
188
 
 
189
    if search is not None:
 
190
        import re
 
191
        searchRE = re.compile(search, re.IGNORECASE)
 
192
    else:
 
193
        searchRE = None
 
194
 
 
195
    which_revs = _enumerate_history(branch)
 
196
    
 
197
    if start_revision is None:
 
198
        start_revision = 1
 
199
    else:
 
200
        branch.check_real_revno(start_revision)
 
201
    
 
202
    if end_revision is None:
 
203
        end_revision = len(which_revs)
 
204
    else:
 
205
        branch.check_real_revno(end_revision)
 
206
 
 
207
    # list indexes are 0-based; revisions are 1-based
 
208
    cut_revs = which_revs[(start_revision-1):(end_revision)]
 
209
    if not cut_revs:
 
210
        return
 
211
    # override the mainline to look like the revision history.
 
212
    mainline_revs = [revision_id for index, revision_id in cut_revs]
 
213
    if cut_revs[0][0] == 1:
 
214
        mainline_revs.insert(0, None)
 
215
    else:
 
216
        mainline_revs.insert(0, which_revs[start_revision-2][1])
 
217
 
 
218
    merge_sorted_revisions = merge_sort(
 
219
        branch.repository.get_revision_graph(mainline_revs[-1]),
 
220
        mainline_revs[-1],
 
221
        mainline_revs)
 
222
 
 
223
    if direction == 'reverse':
 
224
        cut_revs.reverse()
 
225
    elif direction == 'forward':
 
226
        # forward means oldest first.
 
227
        merge_sorted_revisions.reverse()
 
228
    else:
 
229
        raise ValueError('invalid direction %r' % direction)
 
230
 
 
231
    revision_history = branch.revision_history()
 
232
 
 
233
    # convert the revision history to a dictionary:
 
234
    rev_nos = {}
 
235
    for index, rev_id in cut_revs:
 
236
        rev_nos[rev_id] = index
 
237
 
 
238
    # now we just print all the revisions
 
239
    for sequence, rev_id, merge_depth, end_of_merge in merge_sorted_revisions:
 
240
        rev = branch.repository.get_revision(rev_id)
 
241
 
 
242
        if searchRE:
 
243
            if not searchRE.search(rev.message):
 
244
                continue
 
245
 
 
246
        if merge_depth == 0:
 
247
            # a mainline revision.
 
248
            if verbose or specific_fileid:
 
249
                delta = _get_revision_delta(branch, rev_nos[rev_id])
 
250
                
 
251
            if specific_fileid:
 
252
                if not delta.touches_file_id(specific_fileid):
 
253
                    continue
 
254
    
 
255
            if not verbose:
 
256
                # although we calculated it, throw it away without display
 
257
                delta = None
 
258
 
 
259
            lf.show(rev_nos[rev_id], rev, delta)
 
260
        elif hasattr(lf, 'show_merge'):
 
261
            lf.show_merge(rev, merge_depth)
 
262
 
 
263
 
 
264
class LogFormatter(object):
 
265
    """Abstract class to display log messages."""
 
266
 
 
267
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
 
268
        self.to_file = to_file
 
269
        self.show_ids = show_ids
 
270
        self.show_timezone = show_timezone
 
271
 
 
272
    def show(self, revno, rev, delta):
 
273
        raise NotImplementedError('not implemented in abstract base')
 
274
 
 
275
    def short_committer(self, rev):
 
276
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
277
    
 
278
    
 
279
class LongLogFormatter(LogFormatter):
 
280
    def show(self, revno, rev, delta):
 
281
        return self._show_helper(revno=revno, rev=rev, delta=delta)
 
282
 
 
283
    def show_merge(self, rev, merge_depth):
 
284
        return self._show_helper(rev=rev, indent='    '*merge_depth, merged=True, delta=None)
 
285
 
 
286
    def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
 
287
        """Show a revision, either merged or not."""
 
288
        from bzrlib.osutils import format_date
 
289
        to_file = self.to_file
 
290
        print >>to_file,  indent+'-' * 60
 
291
        if revno is not None:
 
292
            print >>to_file,  'revno:', revno
 
293
        if merged:
 
294
            print >>to_file,  indent+'merged:', rev.revision_id
 
295
        elif self.show_ids:
 
296
            print >>to_file,  indent+'revision-id:', rev.revision_id
 
297
        if self.show_ids:
 
298
            for parent_id in rev.parent_ids:
 
299
                print >>to_file, indent+'parent:', parent_id
 
300
        print >>to_file,  indent+'committer:', rev.committer
 
301
        try:
 
302
            print >>to_file, indent+'branch nick: %s' % \
 
303
                rev.properties['branch-nick']
 
304
        except KeyError:
 
305
            pass
 
306
        date_str = format_date(rev.timestamp,
 
307
                               rev.timezone or 0,
 
308
                               self.show_timezone)
 
309
        print >>to_file,  indent+'timestamp: %s' % date_str
 
310
 
 
311
        print >>to_file,  indent+'message:'
 
312
        if not rev.message:
 
313
            print >>to_file,  indent+'  (no message)'
 
314
        else:
 
315
            message = rev.message.rstrip('\r\n')
 
316
            for l in message.split('\n'):
 
317
                print >>to_file,  indent+'  ' + l
 
318
        if delta != None:
 
319
            delta.show(to_file, self.show_ids)
 
320
 
 
321
 
 
322
class ShortLogFormatter(LogFormatter):
 
323
    def show(self, revno, rev, delta):
 
324
        from bzrlib.osutils import format_date
 
325
 
 
326
        to_file = self.to_file
 
327
        date_str = format_date(rev.timestamp, rev.timezone or 0,
 
328
                            self.show_timezone)
 
329
        print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
 
330
                format_date(rev.timestamp, rev.timezone or 0,
 
331
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
332
                           show_offset=False))
 
333
        if self.show_ids:
 
334
            print >>to_file,  '      revision-id:', rev.revision_id
 
335
        if not rev.message:
 
336
            print >>to_file,  '      (no message)'
 
337
        else:
 
338
            message = rev.message.rstrip('\r\n')
 
339
            for l in message.split('\n'):
 
340
                print >>to_file,  '      ' + l
 
341
 
 
342
        # TODO: Why not show the modified files in a shorter form as
 
343
        # well? rewrap them single lines of appropriate length
 
344
        if delta != None:
 
345
            delta.show(to_file, self.show_ids)
 
346
        print >>to_file, ''
 
347
 
 
348
 
 
349
class LineLogFormatter(LogFormatter):
 
350
    def truncate(self, str, max_len):
 
351
        if len(str) <= max_len:
 
352
            return str
 
353
        return str[:max_len-3]+'...'
 
354
 
 
355
    def date_string(self, rev):
 
356
        from bzrlib.osutils import format_date
 
357
        return format_date(rev.timestamp, rev.timezone or 0, 
 
358
                           self.show_timezone, date_fmt="%Y-%m-%d",
 
359
                           show_offset=False)
 
360
 
 
361
    def message(self, rev):
 
362
        if not rev.message:
 
363
            return '(no message)'
 
364
        else:
 
365
            return rev.message
 
366
 
 
367
    def show(self, revno, rev, delta):
 
368
        from bzrlib.osutils import terminal_width
 
369
        print >> self.to_file, self.log_string(revno, rev, terminal_width()-1)
 
370
 
 
371
    def log_string(self, revno, rev, max_chars):
 
372
        """Format log info into one string. Truncate tail of string
 
373
        :param  revno:      revision number (int) or None.
 
374
                            Revision numbers counts from 1.
 
375
        :param  rev:        revision info object
 
376
        :param  max_chars:  maximum length of resulting string
 
377
        :return:            formatted truncated string
 
378
        """
 
379
        out = []
 
380
        if revno:
 
381
            # show revno only when is not None
 
382
            out.append("%d:" % revno)
 
383
        out.append(self.truncate(self.short_committer(rev), 20))
 
384
        out.append(self.date_string(rev))
 
385
        out.append(rev.get_summary())
 
386
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
 
387
 
 
388
 
 
389
def line_log(rev, max_chars):
 
390
    lf = LineLogFormatter(None)
 
391
    return lf.log_string(None, rev, max_chars)
 
392
 
 
393
FORMATTERS = {
 
394
              'long': LongLogFormatter,
 
395
              'short': ShortLogFormatter,
 
396
              'line': LineLogFormatter,
 
397
              }
 
398
 
 
399
def register_formatter(name, formatter):
 
400
    FORMATTERS[name] = formatter
 
401
 
 
402
def log_formatter(name, *args, **kwargs):
 
403
    """Construct a formatter from arguments.
 
404
 
 
405
    name -- Name of the formatter to construct; currently 'long', 'short' and
 
406
        'line' are supported.
 
407
    """
 
408
    from bzrlib.errors import BzrCommandError
 
409
    try:
 
410
        return FORMATTERS[name](*args, **kwargs)
 
411
    except KeyError:
 
412
        raise BzrCommandError("unknown log formatter: %r" % name)
 
413
 
 
414
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
415
    # deprecated; for compatibility
 
416
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
417
    lf.show(revno, rev, delta)
 
418
 
 
419
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
 
420
    """Show the change in revision history comparing the old revision history to the new one.
 
421
 
 
422
    :param branch: The branch where the revisions exist
 
423
    :param old_rh: The old revision history
 
424
    :param new_rh: The new revision history
 
425
    :param to_file: A file to write the results to. If None, stdout will be used
 
426
    """
 
427
    if to_file is None:
 
428
        import sys
 
429
        import codecs
 
430
        import bzrlib
 
431
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
 
432
    lf = log_formatter(log_format,
 
433
                       show_ids=False,
 
434
                       to_file=to_file,
 
435
                       show_timezone='original')
 
436
 
 
437
    # This is the first index which is different between
 
438
    # old and new
 
439
    base_idx = None
 
440
    for i in xrange(max(len(new_rh),
 
441
                        len(old_rh))):
 
442
        if (len(new_rh) <= i
 
443
            or len(old_rh) <= i
 
444
            or new_rh[i] != old_rh[i]):
 
445
            base_idx = i
 
446
            break
 
447
 
 
448
    if base_idx is None:
 
449
        to_file.write('Nothing seems to have changed\n')
 
450
        return
 
451
    ## TODO: It might be nice to do something like show_log
 
452
    ##       and show the merged entries. But since this is the
 
453
    ##       removed revisions, it shouldn't be as important
 
454
    if base_idx < len(old_rh):
 
455
        to_file.write('*'*60)
 
456
        to_file.write('\nRemoved Revisions:\n')
 
457
        for i in range(base_idx, len(old_rh)):
 
458
            rev = branch.repository.get_revision(old_rh[i])
 
459
            lf.show(i+1, rev, None)
 
460
        to_file.write('*'*60)
 
461
        to_file.write('\n\n')
 
462
    if base_idx < len(new_rh):
 
463
        to_file.write('Added Revisions:\n')
 
464
        show_log(branch,
 
465
                 lf,
 
466
                 None,
 
467
                 verbose=True,
 
468
                 direction='forward',
 
469
                 start_revision=base_idx+1,
 
470
                 end_revision=len(new_rh),
 
471
                 search=None)
 
472