~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Vincent Ladeuil
  • Date: 2012-07-31 09:22:02 UTC
  • mto: This revision was merged to the branch mainline in revision 6554.
  • Revision ID: v.ladeuil+lp@free.fr-20120731092202-qh9fs6q4p7y4qqmy
Stop using _CompatibleStack now that local config files can be
shared. Save changes when library state goes out of scope or, as a
fallback, when the process ends.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
16
 
19
17
"""Code to show logs of changes.
20
18
 
42
40
 
43
41
In verbose mode we show a summary of what changed in each particular
44
42
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
 
43
relative to its left-most parent, not the delta relative to the last
46
44
logged revision.  So for example if you ask for a verbose log of
47
45
changes touching hello.c you will get a list of those revisions also
48
46
listing other things that were changed in the same revision, but not
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
52
 
# TODO: option to show delta summaries for merged-in revisions
 
50
from __future__ import absolute_import
53
51
 
54
 
from itertools import izip
 
52
import codecs
 
53
from cStringIO import StringIO
 
54
from itertools import (
 
55
    chain,
 
56
    izip,
 
57
    )
55
58
import re
56
 
 
57
 
from bzrlib import (
 
59
import sys
 
60
from warnings import (
 
61
    warn,
 
62
    )
 
63
 
 
64
from bzrlib.lazy_import import lazy_import
 
65
lazy_import(globals(), """
 
66
 
 
67
from bzrlib import (
 
68
    config,
 
69
    controldir,
 
70
    diff,
 
71
    errors,
 
72
    foreign,
 
73
    repository as _mod_repository,
 
74
    revision as _mod_revision,
 
75
    revisionspec,
 
76
    tsort,
 
77
    )
 
78
from bzrlib.i18n import gettext, ngettext
 
79
""")
 
80
 
 
81
from bzrlib import (
 
82
    lazy_regex,
58
83
    registry,
59
 
    symbol_versioning,
60
 
    )
61
 
import bzrlib.errors as errors
62
 
from bzrlib.revisionspec import(
63
 
    RevisionInfo
64
 
    )
65
 
from bzrlib.symbol_versioning import (
66
 
    deprecated_method,
67
 
    zero_eleven,
68
 
    zero_seventeen,
69
 
    )
70
 
from bzrlib.trace import mutter
71
 
from bzrlib.tsort import (
72
 
    merge_sort,
73
 
    topo_sort,
 
84
    )
 
85
from bzrlib.osutils import (
 
86
    format_date,
 
87
    format_date_with_offset_in_original_timezone,
 
88
    get_diff_header_encoding,
 
89
    get_terminal_encoding,
 
90
    terminal_width,
74
91
    )
75
92
 
76
93
 
88
105
    last_ie = None
89
106
    last_path = None
90
107
    revno = 1
91
 
    for revision_id in branch.revision_history():
92
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
93
 
        if file_id in this_inv:
 
108
    graph = branch.repository.get_graph()
 
109
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
110
        [_mod_revision.NULL_REVISION]))
 
111
    for revision_id in reversed(history):
 
112
        this_inv = branch.repository.get_inventory(revision_id)
 
113
        if this_inv.has_id(file_id):
94
114
            this_ie = this_inv[file_id]
95
115
            this_path = this_inv.id2path(file_id)
96
116
        else:
135
155
             start_revision=None,
136
156
             end_revision=None,
137
157
             search=None,
138
 
             limit=None):
 
158
             limit=None,
 
159
             show_diff=False,
 
160
             match=None):
139
161
    """Write out human-readable log of commits to this branch.
140
162
 
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
 
    search
162
 
        If not None, only show revisions with matching commit messages
163
 
 
164
 
    limit
165
 
        If not None or 0, only show limit revisions
166
 
    """
167
 
    branch.lock_read()
 
163
    This function is being retained for backwards compatibility but
 
164
    should not be extended with new parameters. Use the new Logger class
 
165
    instead, eg. Logger(branch, rqst).show(lf), adding parameters to the
 
166
    make_log_request_dict function.
 
167
 
 
168
    :param lf: The LogFormatter object showing the output.
 
169
 
 
170
    :param specific_fileid: If not None, list only the commits affecting the
 
171
        specified file, rather than all commits.
 
172
 
 
173
    :param verbose: If True show added/changed/deleted/renamed files.
 
174
 
 
175
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
 
176
        earliest to latest.
 
177
 
 
178
    :param start_revision: If not None, only show revisions >= start_revision
 
179
 
 
180
    :param end_revision: If not None, only show revisions <= end_revision
 
181
 
 
182
    :param search: If not None, only show revisions with matching commit
 
183
        messages
 
184
 
 
185
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
 
186
        if None or 0.
 
187
 
 
188
    :param show_diff: If True, output a diff after each revision.
 
189
 
 
190
    :param match: Dictionary of search lists to use when matching revision
 
191
      properties.
 
192
    """
 
193
    # Convert old-style parameters to new-style parameters
 
194
    if specific_fileid is not None:
 
195
        file_ids = [specific_fileid]
 
196
    else:
 
197
        file_ids = None
 
198
    if verbose:
 
199
        if file_ids:
 
200
            delta_type = 'partial'
 
201
        else:
 
202
            delta_type = 'full'
 
203
    else:
 
204
        delta_type = None
 
205
    if show_diff:
 
206
        if file_ids:
 
207
            diff_type = 'partial'
 
208
        else:
 
209
            diff_type = 'full'
 
210
    else:
 
211
        diff_type = None
 
212
 
 
213
    # Build the request and execute it
 
214
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
 
215
        start_revision=start_revision, end_revision=end_revision,
 
216
        limit=limit, message_search=search,
 
217
        delta_type=delta_type, diff_type=diff_type)
 
218
    Logger(branch, rqst).show(lf)
 
219
 
 
220
 
 
221
# Note: This needs to be kept in sync with the defaults in
 
222
# make_log_request_dict() below
 
223
_DEFAULT_REQUEST_PARAMS = {
 
224
    'direction': 'reverse',
 
225
    'levels': None,
 
226
    'generate_tags': True,
 
227
    'exclude_common_ancestry': False,
 
228
    '_match_using_deltas': True,
 
229
    }
 
230
 
 
231
 
 
232
def make_log_request_dict(direction='reverse', specific_fileids=None,
 
233
                          start_revision=None, end_revision=None, limit=None,
 
234
                          message_search=None, levels=None, generate_tags=True,
 
235
                          delta_type=None,
 
236
                          diff_type=None, _match_using_deltas=True,
 
237
                          exclude_common_ancestry=False, match=None,
 
238
                          signature=False, omit_merges=False,
 
239
                          ):
 
240
    """Convenience function for making a logging request dictionary.
 
241
 
 
242
    Using this function may make code slightly safer by ensuring
 
243
    parameters have the correct names. It also provides a reference
 
244
    point for documenting the supported parameters.
 
245
 
 
246
    :param direction: 'reverse' (default) is latest to earliest;
 
247
      'forward' is earliest to latest.
 
248
 
 
249
    :param specific_fileids: If not None, only include revisions
 
250
      affecting the specified files, rather than all revisions.
 
251
 
 
252
    :param start_revision: If not None, only generate
 
253
      revisions >= start_revision
 
254
 
 
255
    :param end_revision: If not None, only generate
 
256
      revisions <= end_revision
 
257
 
 
258
    :param limit: If set, generate only 'limit' revisions, all revisions
 
259
      are shown if None or 0.
 
260
 
 
261
    :param message_search: If not None, only include revisions with
 
262
      matching commit messages
 
263
 
 
264
    :param levels: the number of levels of revisions to
 
265
      generate; 1 for just the mainline; 0 for all levels, or None for
 
266
      a sensible default.
 
267
 
 
268
    :param generate_tags: If True, include tags for matched revisions.
 
269
`
 
270
    :param delta_type: Either 'full', 'partial' or None.
 
271
      'full' means generate the complete delta - adds/deletes/modifies/etc;
 
272
      'partial' means filter the delta using specific_fileids;
 
273
      None means do not generate any delta.
 
274
 
 
275
    :param diff_type: Either 'full', 'partial' or None.
 
276
      'full' means generate the complete diff - adds/deletes/modifies/etc;
 
277
      'partial' means filter the diff using specific_fileids;
 
278
      None means do not generate any diff.
 
279
 
 
280
    :param _match_using_deltas: a private parameter controlling the
 
281
      algorithm used for matching specific_fileids. This parameter
 
282
      may be removed in the future so bzrlib client code should NOT
 
283
      use it.
 
284
 
 
285
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
286
      range operator or as a graph difference.
 
287
 
 
288
    :param signature: show digital signature information
 
289
 
 
290
    :param match: Dictionary of list of search strings to use when filtering
 
291
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
292
      the empty string to match any of the preceding properties.
 
293
 
 
294
    :param omit_merges: If True, commits with more than one parent are
 
295
      omitted.
 
296
 
 
297
    """
 
298
    # Take care of old style message_search parameter
 
299
    if message_search:
 
300
        if match:
 
301
            if 'message' in match:
 
302
                match['message'].append(message_search)
 
303
            else:
 
304
                match['message'] = [message_search]
 
305
        else:
 
306
            match={ 'message': [message_search] }
 
307
    return {
 
308
        'direction': direction,
 
309
        'specific_fileids': specific_fileids,
 
310
        'start_revision': start_revision,
 
311
        'end_revision': end_revision,
 
312
        'limit': limit,
 
313
        'levels': levels,
 
314
        'generate_tags': generate_tags,
 
315
        'delta_type': delta_type,
 
316
        'diff_type': diff_type,
 
317
        'exclude_common_ancestry': exclude_common_ancestry,
 
318
        'signature': signature,
 
319
        'match': match,
 
320
        'omit_merges': omit_merges,
 
321
        # Add 'private' attributes for features that may be deprecated
 
322
        '_match_using_deltas': _match_using_deltas,
 
323
    }
 
324
 
 
325
 
 
326
def _apply_log_request_defaults(rqst):
 
327
    """Apply default values to a request dictionary."""
 
328
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
329
    if rqst:
 
330
        result.update(rqst)
 
331
    return result
 
332
 
 
333
 
 
334
def format_signature_validity(rev_id, repo):
 
335
    """get the signature validity
 
336
 
 
337
    :param rev_id: revision id to validate
 
338
    :param repo: repository of revision
 
339
    :return: human readable string to print to log
 
340
    """
 
341
    from bzrlib import gpg
 
342
 
 
343
    gpg_strategy = gpg.GPGStrategy(None)
 
344
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
 
345
    if result[0] == gpg.SIGNATURE_VALID:
 
346
        return "valid signature from {0}".format(result[1])
 
347
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
348
        return "unknown key {0}".format(result[1])
 
349
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
350
        return "invalid signature!"
 
351
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
352
        return "no signature"
 
353
 
 
354
 
 
355
class LogGenerator(object):
 
356
    """A generator of log revisions."""
 
357
 
 
358
    def iter_log_revisions(self):
 
359
        """Iterate over LogRevision objects.
 
360
 
 
361
        :return: An iterator yielding LogRevision objects.
 
362
        """
 
363
        raise NotImplementedError(self.iter_log_revisions)
 
364
 
 
365
 
 
366
class Logger(object):
 
367
    """An object that generates, formats and displays a log."""
 
368
 
 
369
    def __init__(self, branch, rqst):
 
370
        """Create a Logger.
 
371
 
 
372
        :param branch: the branch to log
 
373
        :param rqst: A dictionary specifying the query parameters.
 
374
          See make_log_request_dict() for supported values.
 
375
        """
 
376
        self.branch = branch
 
377
        self.rqst = _apply_log_request_defaults(rqst)
 
378
 
 
379
    def show(self, lf):
 
380
        """Display the log.
 
381
 
 
382
        :param lf: The LogFormatter object to send the output to.
 
383
        """
 
384
        if not isinstance(lf, LogFormatter):
 
385
            warn("not a LogFormatter instance: %r" % lf)
 
386
 
 
387
        self.branch.lock_read()
 
388
        try:
 
389
            if getattr(lf, 'begin_log', None):
 
390
                lf.begin_log()
 
391
            self._show_body(lf)
 
392
            if getattr(lf, 'end_log', None):
 
393
                lf.end_log()
 
394
        finally:
 
395
            self.branch.unlock()
 
396
 
 
397
    def _show_body(self, lf):
 
398
        """Show the main log output.
 
399
 
 
400
        Subclasses may wish to override this.
 
401
        """
 
402
        # Tweak the LogRequest based on what the LogFormatter can handle.
 
403
        # (There's no point generating stuff if the formatter can't display it.)
 
404
        rqst = self.rqst
 
405
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
406
            # user didn't specify levels, use whatever the LF can handle:
 
407
            rqst['levels'] = lf.get_levels()
 
408
 
 
409
        if not getattr(lf, 'supports_tags', False):
 
410
            rqst['generate_tags'] = False
 
411
        if not getattr(lf, 'supports_delta', False):
 
412
            rqst['delta_type'] = None
 
413
        if not getattr(lf, 'supports_diff', False):
 
414
            rqst['diff_type'] = None
 
415
        if not getattr(lf, 'supports_signatures', False):
 
416
            rqst['signature'] = False
 
417
 
 
418
        # Find and print the interesting revisions
 
419
        generator = self._generator_factory(self.branch, rqst)
 
420
        for lr in generator.iter_log_revisions():
 
421
            lf.log_revision(lr)
 
422
        lf.show_advice()
 
423
 
 
424
    def _generator_factory(self, branch, rqst):
 
425
        """Make the LogGenerator object to use.
 
426
 
 
427
        Subclasses may wish to override this.
 
428
        """
 
429
        return _DefaultLogGenerator(branch, rqst)
 
430
 
 
431
 
 
432
class _StartNotLinearAncestor(Exception):
 
433
    """Raised when a start revision is not found walking left-hand history."""
 
434
 
 
435
 
 
436
class _DefaultLogGenerator(LogGenerator):
 
437
    """The default generator of log revisions."""
 
438
 
 
439
    def __init__(self, branch, rqst):
 
440
        self.branch = branch
 
441
        self.rqst = rqst
 
442
        if rqst.get('generate_tags') and branch.supports_tags():
 
443
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
 
444
        else:
 
445
            self.rev_tag_dict = {}
 
446
 
 
447
    def iter_log_revisions(self):
 
448
        """Iterate over LogRevision objects.
 
449
 
 
450
        :return: An iterator yielding LogRevision objects.
 
451
        """
 
452
        rqst = self.rqst
 
453
        levels = rqst.get('levels')
 
454
        limit = rqst.get('limit')
 
455
        diff_type = rqst.get('diff_type')
 
456
        show_signature = rqst.get('signature')
 
457
        omit_merges = rqst.get('omit_merges')
 
458
        log_count = 0
 
459
        revision_iterator = self._create_log_revision_iterator()
 
460
        for revs in revision_iterator:
 
461
            for (rev_id, revno, merge_depth), rev, delta in revs:
 
462
                # 0 levels means show everything; merge_depth counts from 0
 
463
                if levels != 0 and merge_depth >= levels:
 
464
                    continue
 
465
                if omit_merges and len(rev.parent_ids) > 1:
 
466
                    continue
 
467
                if diff_type is None:
 
468
                    diff = None
 
469
                else:
 
470
                    diff = self._format_diff(rev, rev_id, diff_type)
 
471
                if show_signature:
 
472
                    signature = format_signature_validity(rev_id,
 
473
                                                self.branch.repository)
 
474
                else:
 
475
                    signature = None
 
476
                yield LogRevision(rev, revno, merge_depth, delta,
 
477
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
478
                if limit:
 
479
                    log_count += 1
 
480
                    if log_count >= limit:
 
481
                        return
 
482
 
 
483
    def _format_diff(self, rev, rev_id, diff_type):
 
484
        repo = self.branch.repository
 
485
        if len(rev.parent_ids) == 0:
 
486
            ancestor_id = _mod_revision.NULL_REVISION
 
487
        else:
 
488
            ancestor_id = rev.parent_ids[0]
 
489
        tree_1 = repo.revision_tree(ancestor_id)
 
490
        tree_2 = repo.revision_tree(rev_id)
 
491
        file_ids = self.rqst.get('specific_fileids')
 
492
        if diff_type == 'partial' and file_ids is not None:
 
493
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
494
        else:
 
495
            specific_files = None
 
496
        s = StringIO()
 
497
        path_encoding = get_diff_header_encoding()
 
498
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
499
            new_label='', path_encoding=path_encoding)
 
500
        return s.getvalue()
 
501
 
 
502
    def _create_log_revision_iterator(self):
 
503
        """Create a revision iterator for log.
 
504
 
 
505
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
506
            delta).
 
507
        """
 
508
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
509
            self.branch, self.rqst.get('start_revision'),
 
510
            self.rqst.get('end_revision'))
 
511
        if self.rqst.get('_match_using_deltas'):
 
512
            return self._log_revision_iterator_using_delta_matching()
 
513
        else:
 
514
            # We're using the per-file-graph algorithm. This scales really
 
515
            # well but only makes sense if there is a single file and it's
 
516
            # not a directory
 
517
            file_count = len(self.rqst.get('specific_fileids'))
 
518
            if file_count != 1:
 
519
                raise BzrError("illegal LogRequest: must match-using-deltas "
 
520
                    "when logging %d files" % file_count)
 
521
            return self._log_revision_iterator_using_per_file_graph()
 
522
 
 
523
    def _log_revision_iterator_using_delta_matching(self):
 
524
        # Get the base revisions, filtering by the revision range
 
525
        rqst = self.rqst
 
526
        generate_merge_revisions = rqst.get('levels') != 1
 
527
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
528
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
529
        view_revisions = _calc_view_revisions(
 
530
            self.branch, self.start_rev_id, self.end_rev_id,
 
531
            rqst.get('direction'),
 
532
            generate_merge_revisions=generate_merge_revisions,
 
533
            delayed_graph_generation=delayed_graph_generation,
 
534
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
535
 
 
536
        # Apply the other filters
 
537
        return make_log_rev_iterator(self.branch, view_revisions,
 
538
            rqst.get('delta_type'), rqst.get('match'),
 
539
            file_ids=rqst.get('specific_fileids'),
 
540
            direction=rqst.get('direction'))
 
541
 
 
542
    def _log_revision_iterator_using_per_file_graph(self):
 
543
        # Get the base revisions, filtering by the revision range.
 
544
        # Note that we always generate the merge revisions because
 
545
        # filter_revisions_touching_file_id() requires them ...
 
546
        rqst = self.rqst
 
547
        view_revisions = _calc_view_revisions(
 
548
            self.branch, self.start_rev_id, self.end_rev_id,
 
549
            rqst.get('direction'), generate_merge_revisions=True,
 
550
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
551
        if not isinstance(view_revisions, list):
 
552
            view_revisions = list(view_revisions)
 
553
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
554
            rqst.get('specific_fileids')[0], view_revisions,
 
555
            include_merges=rqst.get('levels') != 1)
 
556
        return make_log_rev_iterator(self.branch, view_revisions,
 
557
            rqst.get('delta_type'), rqst.get('match'))
 
558
 
 
559
 
 
560
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
 
561
                         generate_merge_revisions,
 
562
                         delayed_graph_generation=False,
 
563
                         exclude_common_ancestry=False,
 
564
                         ):
 
565
    """Calculate the revisions to view.
 
566
 
 
567
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
568
             a list of the same tuples.
 
569
    """
 
570
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
571
        raise errors.BzrCommandError(gettext(
 
572
            '--exclude-common-ancestry requires two different revisions'))
 
573
    if direction not in ('reverse', 'forward'):
 
574
        raise ValueError(gettext('invalid direction %r') % direction)
 
575
    br_revno, br_rev_id = branch.last_revision_info()
 
576
    if br_revno == 0:
 
577
        return []
 
578
 
 
579
    if (end_rev_id and start_rev_id == end_rev_id
 
580
        and (not generate_merge_revisions
 
581
             or not _has_merges(branch, end_rev_id))):
 
582
        # If a single revision is requested, check we can handle it
 
583
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
584
                                       br_revno)
 
585
    if not generate_merge_revisions:
 
586
        try:
 
587
            # If we only want to see linear revisions, we can iterate ...
 
588
            iter_revs = _linear_view_revisions(
 
589
                branch, start_rev_id, end_rev_id,
 
590
                exclude_common_ancestry=exclude_common_ancestry)
 
591
            # If a start limit was given and it's not obviously an
 
592
            # ancestor of the end limit, check it before outputting anything
 
593
            if (direction == 'forward'
 
594
                or (start_rev_id and not _is_obvious_ancestor(
 
595
                        branch, start_rev_id, end_rev_id))):
 
596
                    iter_revs = list(iter_revs)
 
597
            if direction == 'forward':
 
598
                iter_revs = reversed(iter_revs)
 
599
            return iter_revs
 
600
        except _StartNotLinearAncestor:
 
601
            # Switch to the slower implementation that may be able to find a
 
602
            # non-obvious ancestor out of the left-hand history.
 
603
            pass
 
604
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
605
                                        direction, delayed_graph_generation,
 
606
                                        exclude_common_ancestry)
 
607
    if direction == 'forward':
 
608
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
609
    return iter_revs
 
610
 
 
611
 
 
612
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
 
613
    if rev_id == br_rev_id:
 
614
        # It's the tip
 
615
        return [(br_rev_id, br_revno, 0)]
 
616
    else:
 
617
        revno_str = _compute_revno_str(branch, rev_id)
 
618
        return [(rev_id, revno_str, 0)]
 
619
 
 
620
 
 
621
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
 
622
                            delayed_graph_generation,
 
623
                            exclude_common_ancestry=False):
 
624
    # On large trees, generating the merge graph can take 30-60 seconds
 
625
    # so we delay doing it until a merge is detected, incrementally
 
626
    # returning initial (non-merge) revisions while we can.
 
627
 
 
628
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
629
    # couple of seconds only should be needed to load the whole graph and the
 
630
    # other graph operations needed are even faster than that -- vila 100201
 
631
    initial_revisions = []
 
632
    if delayed_graph_generation:
 
633
        try:
 
634
            for rev_id, revno, depth in  _linear_view_revisions(
 
635
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
636
                if _has_merges(branch, rev_id):
 
637
                    # The end_rev_id can be nested down somewhere. We need an
 
638
                    # explicit ancestry check. There is an ambiguity here as we
 
639
                    # may not raise _StartNotLinearAncestor for a revision that
 
640
                    # is an ancestor but not a *linear* one. But since we have
 
641
                    # loaded the graph to do the check (or calculate a dotted
 
642
                    # revno), we may as well accept to show the log...  We need
 
643
                    # the check only if start_rev_id is not None as all
 
644
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
645
                    # -- vila 20100319
 
646
                    graph = branch.repository.get_graph()
 
647
                    if (start_rev_id is not None
 
648
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
649
                        raise _StartNotLinearAncestor()
 
650
                    # Since we collected the revisions so far, we need to
 
651
                    # adjust end_rev_id.
 
652
                    end_rev_id = rev_id
 
653
                    break
 
654
                else:
 
655
                    initial_revisions.append((rev_id, revno, depth))
 
656
            else:
 
657
                # No merged revisions found
 
658
                return initial_revisions
 
659
        except _StartNotLinearAncestor:
 
660
            # A merge was never detected so the lower revision limit can't
 
661
            # be nested down somewhere
 
662
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
663
                ' history of end revision.'))
 
664
 
 
665
    # We exit the loop above because we encounter a revision with merges, from
 
666
    # this revision, we need to switch to _graph_view_revisions.
 
667
 
 
668
    # A log including nested merges is required. If the direction is reverse,
 
669
    # we rebase the initial merge depths so that the development line is
 
670
    # shown naturally, i.e. just like it is for linear logging. We can easily
 
671
    # make forward the exact opposite display, but showing the merge revisions
 
672
    # indented at the end seems slightly nicer in that case.
 
673
    view_revisions = chain(iter(initial_revisions),
 
674
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
675
                              rebase_initial_depths=(direction == 'reverse'),
 
676
                              exclude_common_ancestry=exclude_common_ancestry))
 
677
    return view_revisions
 
678
 
 
679
 
 
680
def _has_merges(branch, rev_id):
 
681
    """Does a revision have multiple parents or not?"""
 
682
    parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
 
683
    return len(parents) > 1
 
684
 
 
685
 
 
686
def _compute_revno_str(branch, rev_id):
 
687
    """Compute the revno string from a rev_id.
 
688
 
 
689
    :return: The revno string, or None if the revision is not in the supplied
 
690
        branch.
 
691
    """
168
692
    try:
169
 
        if getattr(lf, 'begin_log', None):
170
 
            lf.begin_log()
171
 
 
172
 
        _show_log(branch, lf, specific_fileid, verbose, direction,
173
 
                  start_revision, end_revision, search, limit)
174
 
 
175
 
        if getattr(lf, 'end_log', None):
176
 
            lf.end_log()
177
 
    finally:
178
 
        branch.unlock()
179
 
 
180
 
def _show_log(branch,
181
 
             lf,
182
 
             specific_fileid=None,
183
 
             verbose=False,
184
 
             direction='reverse',
185
 
             start_revision=None,
186
 
             end_revision=None,
187
 
             search=None,
188
 
             limit=None):
189
 
    """Worker function for show_log - see show_log."""
190
 
    from bzrlib.osutils import format_date
191
 
    from bzrlib.errors import BzrCheckError
192
 
    
193
 
    from warnings import warn
194
 
 
195
 
    if not isinstance(lf, LogFormatter):
196
 
        warn("not a LogFormatter instance: %r" % lf)
197
 
 
198
 
    if specific_fileid:
199
 
        mutter('get log for file_id %r', specific_fileid)
200
 
 
201
 
    if search is not None:
202
 
        import re
203
 
        searchRE = re.compile(search, re.IGNORECASE)
204
 
    else:
205
 
        searchRE = None
206
 
 
207
 
    mainline_revs, rev_nos, start_rev_id, end_rev_id = \
208
 
        _get_mainline_revs(branch, start_revision, end_revision)
209
 
    if not mainline_revs:
210
 
        return
211
 
 
212
 
    if direction == 'reverse':
213
 
        start_rev_id, end_rev_id = end_rev_id, start_rev_id
214
 
        
215
 
    legacy_lf = getattr(lf, 'log_revision', None) is None
216
 
    if legacy_lf:
217
 
        # pre-0.17 formatters use show for mainline revisions.
218
 
        # how should we show merged revisions ?
219
 
        #   pre-0.11 api: show_merge
220
 
        #   0.11-0.16 api: show_merge_revno
221
 
        show_merge_revno = getattr(lf, 'show_merge_revno', None)
222
 
        show_merge = getattr(lf, 'show_merge', None)
223
 
        if show_merge is None and show_merge_revno is None:
224
 
            # no merged-revno support
225
 
            generate_merge_revisions = False
226
 
        else:
227
 
            generate_merge_revisions = True
228
 
        # tell developers to update their code
229
 
        symbol_versioning.warn('LogFormatters should provide log_revision '
230
 
            'instead of show and show_merge_revno since bzr 0.17.',
231
 
            DeprecationWarning, stacklevel=3)
232
 
    else:
233
 
        generate_merge_revisions = getattr(lf, 'supports_merge_revisions', 
234
 
                                           False)
235
 
    view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
236
 
                          direction, include_merges=generate_merge_revisions)
237
 
    view_revisions = _filter_revision_range(list(view_revs_iter),
238
 
                                            start_rev_id,
239
 
                                            end_rev_id)
240
 
    if specific_fileid:
241
 
        view_revisions = _filter_revisions_touching_file_id(branch,
242
 
                                                         specific_fileid,
243
 
                                                         mainline_revs,
244
 
                                                         view_revisions)
245
 
 
246
 
    rev_tag_dict = {}
247
 
    generate_tags = getattr(lf, 'supports_tags', False)
248
 
    if generate_tags:
249
 
        if branch.supports_tags():
250
 
            rev_tag_dict = branch.tags.get_reverse_tag_dict()
251
 
 
252
 
    generate_delta = verbose and getattr(lf, 'supports_delta', False)
253
 
 
254
 
    def iter_revisions():
255
 
        # r = revision, n = revno, d = merge depth
256
 
        revision_ids = [r for r, n, d in view_revisions]
257
 
        num = 9
258
 
        repository = branch.repository
259
 
        while revision_ids:
260
 
            cur_deltas = {}
261
 
            revisions = repository.get_revisions(revision_ids[:num])
262
 
            if generate_delta:
263
 
                deltas = repository.get_deltas_for_revisions(revisions)
264
 
                cur_deltas = dict(izip((r.revision_id for r in revisions),
265
 
                                       deltas))
266
 
            for revision in revisions:
267
 
                yield revision, cur_deltas.get(revision.revision_id)
268
 
            revision_ids  = revision_ids[num:]
 
693
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
694
    except errors.NoSuchRevision:
 
695
        # The revision must be outside of this branch
 
696
        return None
 
697
    else:
 
698
        return '.'.join(str(n) for n in revno)
 
699
 
 
700
 
 
701
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
 
702
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
 
703
    if start_rev_id and end_rev_id:
 
704
        try:
 
705
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
706
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
707
        except errors.NoSuchRevision:
 
708
            # one or both is not in the branch; not obvious
 
709
            return False
 
710
        if len(start_dotted) == 1 and len(end_dotted) == 1:
 
711
            # both on mainline
 
712
            return start_dotted[0] <= end_dotted[0]
 
713
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
 
714
            start_dotted[0:1] == end_dotted[0:1]):
 
715
            # both on same development line
 
716
            return start_dotted[2] <= end_dotted[2]
 
717
        else:
 
718
            # not obvious
 
719
            return False
 
720
    # if either start or end is not specified then we use either the first or
 
721
    # the last revision and *they* are obvious ancestors.
 
722
    return True
 
723
 
 
724
 
 
725
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
726
                           exclude_common_ancestry=False):
 
727
    """Calculate a sequence of revisions to view, newest to oldest.
 
728
 
 
729
    :param start_rev_id: the lower revision-id
 
730
    :param end_rev_id: the upper revision-id
 
731
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
732
        the iterated revisions.
 
733
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
734
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
 
735
        is not found walking the left-hand history
 
736
    """
 
737
    br_revno, br_rev_id = branch.last_revision_info()
 
738
    repo = branch.repository
 
739
    graph = repo.get_graph()
 
740
    if start_rev_id is None and end_rev_id is None:
 
741
        cur_revno = br_revno
 
742
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
743
            (_mod_revision.NULL_REVISION,)):
 
744
            yield revision_id, str(cur_revno), 0
 
745
            cur_revno -= 1
 
746
    else:
 
747
        if end_rev_id is None:
 
748
            end_rev_id = br_rev_id
 
749
        found_start = start_rev_id is None
 
750
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
751
                (_mod_revision.NULL_REVISION,)):
 
752
            revno_str = _compute_revno_str(branch, revision_id)
 
753
            if not found_start and revision_id == start_rev_id:
 
754
                if not exclude_common_ancestry:
 
755
                    yield revision_id, revno_str, 0
 
756
                found_start = True
 
757
                break
 
758
            else:
 
759
                yield revision_id, revno_str, 0
 
760
        else:
 
761
            if not found_start:
 
762
                raise _StartNotLinearAncestor()
 
763
 
 
764
 
 
765
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
766
                          rebase_initial_depths=True,
 
767
                          exclude_common_ancestry=False):
 
768
    """Calculate revisions to view including merges, newest to oldest.
 
769
 
 
770
    :param branch: the branch
 
771
    :param start_rev_id: the lower revision-id
 
772
    :param end_rev_id: the upper revision-id
 
773
    :param rebase_initial_depth: should depths be rebased until a mainline
 
774
      revision is found?
 
775
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
776
    """
 
777
    if exclude_common_ancestry:
 
778
        stop_rule = 'with-merges-without-common-ancestry'
 
779
    else:
 
780
        stop_rule = 'with-merges'
 
781
    view_revisions = branch.iter_merge_sorted_revisions(
 
782
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
 
783
        stop_rule=stop_rule)
 
784
    if not rebase_initial_depths:
 
785
        for (rev_id, merge_depth, revno, end_of_merge
 
786
             ) in view_revisions:
 
787
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
788
    else:
 
789
        # We're following a development line starting at a merged revision.
 
790
        # We need to adjust depths down by the initial depth until we find
 
791
        # a depth less than it. Then we use that depth as the adjustment.
 
792
        # If and when we reach the mainline, depth adjustment ends.
 
793
        depth_adjustment = None
 
794
        for (rev_id, merge_depth, revno, end_of_merge
 
795
             ) in view_revisions:
 
796
            if depth_adjustment is None:
 
797
                depth_adjustment = merge_depth
 
798
            if depth_adjustment:
 
799
                if merge_depth < depth_adjustment:
 
800
                    # From now on we reduce the depth adjustement, this can be
 
801
                    # surprising for users. The alternative requires two passes
 
802
                    # which breaks the fast display of the first revision
 
803
                    # though.
 
804
                    depth_adjustment = merge_depth
 
805
                merge_depth -= depth_adjustment
 
806
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
807
 
 
808
 
 
809
def _rebase_merge_depth(view_revisions):
 
810
    """Adjust depths upwards so the top level is 0."""
 
811
    # If either the first or last revision have a merge_depth of 0, we're done
 
812
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
 
813
        min_depth = min([d for r,n,d in view_revisions])
 
814
        if min_depth != 0:
 
815
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
 
816
    return view_revisions
 
817
 
 
818
 
 
819
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
 
820
        file_ids=None, direction='reverse'):
 
821
    """Create a revision iterator for log.
 
822
 
 
823
    :param branch: The branch being logged.
 
824
    :param view_revisions: The revisions being viewed.
 
825
    :param generate_delta: Whether to generate a delta for each revision.
 
826
      Permitted values are None, 'full' and 'partial'.
 
827
    :param search: A user text search string.
 
828
    :param file_ids: If non empty, only revisions matching one or more of
 
829
      the file-ids are to be kept.
 
830
    :param direction: the direction in which view_revisions is sorted
 
831
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
832
        delta).
 
833
    """
 
834
    # Convert view_revisions into (view, None, None) groups to fit with
 
835
    # the standard interface here.
 
836
    if type(view_revisions) == list:
 
837
        # A single batch conversion is faster than many incremental ones.
 
838
        # As we have all the data, do a batch conversion.
 
839
        nones = [None] * len(view_revisions)
 
840
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
 
841
    else:
 
842
        def _convert():
 
843
            for view in view_revisions:
 
844
                yield (view, None, None)
 
845
        log_rev_iterator = iter([_convert()])
 
846
    for adapter in log_adapters:
 
847
        # It would be nicer if log adapters were first class objects
 
848
        # with custom parameters. This will do for now. IGC 20090127
 
849
        if adapter == _make_delta_filter:
 
850
            log_rev_iterator = adapter(branch, generate_delta,
 
851
                search, log_rev_iterator, file_ids, direction)
 
852
        else:
 
853
            log_rev_iterator = adapter(branch, generate_delta,
 
854
                search, log_rev_iterator)
 
855
    return log_rev_iterator
 
856
 
 
857
 
 
858
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
859
    """Create a filtered iterator of log_rev_iterator matching on a regex.
 
860
 
 
861
    :param branch: The branch being logged.
 
862
    :param generate_delta: Whether to generate a delta for each revision.
 
863
    :param match: A dictionary with properties as keys and lists of strings
 
864
        as values. To match, a revision may match any of the supplied strings
 
865
        within a single property but must match at least one string for each
 
866
        property.
 
867
    :param log_rev_iterator: An input iterator containing all revisions that
 
868
        could be displayed, in lists.
 
869
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
870
        delta).
 
871
    """
 
872
    if match is None:
 
873
        return log_rev_iterator
 
874
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
875
                for (k,v) in match.iteritems()]
 
876
    return _filter_re(searchRE, log_rev_iterator)
 
877
 
 
878
 
 
879
def _filter_re(searchRE, log_rev_iterator):
 
880
    for revs in log_rev_iterator:
 
881
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
882
        if new_revs:
 
883
            yield new_revs
 
884
 
 
885
def _match_filter(searchRE, rev):
 
886
    strings = {
 
887
               'message': (rev.message,),
 
888
               'committer': (rev.committer,),
 
889
               'author': (rev.get_apparent_authors()),
 
890
               'bugs': list(rev.iter_bugs())
 
891
               }
 
892
    strings[''] = [item for inner_list in strings.itervalues()
 
893
                   for item in inner_list]
 
894
    for (k,v) in searchRE:
 
895
        if k in strings and not _match_any_filter(strings[k], v):
 
896
            return False
 
897
    return True
 
898
 
 
899
def _match_any_filter(strings, res):
 
900
    return any([filter(None, map(re.search, strings)) for re in res])
 
901
 
 
902
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
 
903
    fileids=None, direction='reverse'):
 
904
    """Add revision deltas to a log iterator if needed.
 
905
 
 
906
    :param branch: The branch being logged.
 
907
    :param generate_delta: Whether to generate a delta for each revision.
 
908
      Permitted values are None, 'full' and 'partial'.
 
909
    :param search: A user text search string.
 
910
    :param log_rev_iterator: An input iterator containing all revisions that
 
911
        could be displayed, in lists.
 
912
    :param fileids: If non empty, only revisions matching one or more of
 
913
      the file-ids are to be kept.
 
914
    :param direction: the direction in which view_revisions is sorted
 
915
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
916
        delta).
 
917
    """
 
918
    if not generate_delta and not fileids:
 
919
        return log_rev_iterator
 
920
    return _generate_deltas(branch.repository, log_rev_iterator,
 
921
        generate_delta, fileids, direction)
 
922
 
 
923
 
 
924
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
925
    direction):
 
926
    """Create deltas for each batch of revisions in log_rev_iterator.
 
927
 
 
928
    If we're only generating deltas for the sake of filtering against
 
929
    file-ids, we stop generating deltas once all file-ids reach the
 
930
    appropriate life-cycle point. If we're receiving data newest to
 
931
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
 
932
    """
 
933
    check_fileids = fileids is not None and len(fileids) > 0
 
934
    if check_fileids:
 
935
        fileid_set = set(fileids)
 
936
        if direction == 'reverse':
 
937
            stop_on = 'add'
 
938
        else:
 
939
            stop_on = 'remove'
 
940
    else:
 
941
        fileid_set = None
 
942
    for revs in log_rev_iterator:
 
943
        # If we were matching against fileids and we've run out,
 
944
        # there's nothing left to do
 
945
        if check_fileids and not fileid_set:
 
946
            return
 
947
        revisions = [rev[1] for rev in revs]
 
948
        new_revs = []
 
949
        if delta_type == 'full' and not check_fileids:
 
950
            deltas = repository.get_deltas_for_revisions(revisions)
 
951
            for rev, delta in izip(revs, deltas):
 
952
                new_revs.append((rev[0], rev[1], delta))
 
953
        else:
 
954
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
 
955
            for rev, delta in izip(revs, deltas):
 
956
                if check_fileids:
 
957
                    if delta is None or not delta.has_changed():
 
958
                        continue
 
959
                    else:
 
960
                        _update_fileids(delta, fileid_set, stop_on)
 
961
                        if delta_type is None:
 
962
                            delta = None
 
963
                        elif delta_type == 'full':
 
964
                            # If the file matches all the time, rebuilding
 
965
                            # a full delta like this in addition to a partial
 
966
                            # one could be slow. However, it's likely that
 
967
                            # most revisions won't get this far, making it
 
968
                            # faster to filter on the partial deltas and
 
969
                            # build the occasional full delta than always
 
970
                            # building full deltas and filtering those.
 
971
                            rev_id = rev[0][0]
 
972
                            delta = repository.get_revision_delta(rev_id)
 
973
                new_revs.append((rev[0], rev[1], delta))
 
974
        yield new_revs
 
975
 
 
976
 
 
977
def _update_fileids(delta, fileids, stop_on):
 
978
    """Update the set of file-ids to search based on file lifecycle events.
 
979
 
 
980
    :param fileids: a set of fileids to update
 
981
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
982
      fileids set once their add or remove entry is detected respectively
 
983
    """
 
984
    if stop_on == 'add':
 
985
        for item in delta.added:
 
986
            if item[1] in fileids:
 
987
                fileids.remove(item[1])
 
988
    elif stop_on == 'delete':
 
989
        for item in delta.removed:
 
990
            if item[1] in fileids:
 
991
                fileids.remove(item[1])
 
992
 
 
993
 
 
994
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
 
995
    """Extract revision objects from the repository
 
996
 
 
997
    :param branch: The branch being logged.
 
998
    :param generate_delta: Whether to generate a delta for each revision.
 
999
    :param search: A user text search string.
 
1000
    :param log_rev_iterator: An input iterator containing all revisions that
 
1001
        could be displayed, in lists.
 
1002
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1003
        delta).
 
1004
    """
 
1005
    repository = branch.repository
 
1006
    for revs in log_rev_iterator:
 
1007
        # r = revision_id, n = revno, d = merge depth
 
1008
        revision_ids = [view[0] for view, _, _ in revs]
 
1009
        revisions = repository.get_revisions(revision_ids)
 
1010
        revs = [(rev[0], revision, rev[2]) for rev, revision in
 
1011
            izip(revs, revisions)]
 
1012
        yield revs
 
1013
 
 
1014
 
 
1015
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
 
1016
    """Group up a single large batch into smaller ones.
 
1017
 
 
1018
    :param branch: The branch being logged.
 
1019
    :param generate_delta: Whether to generate a delta for each revision.
 
1020
    :param search: A user text search string.
 
1021
    :param log_rev_iterator: An input iterator containing all revisions that
 
1022
        could be displayed, in lists.
 
1023
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1024
        delta).
 
1025
    """
 
1026
    num = 9
 
1027
    for batch in log_rev_iterator:
 
1028
        batch = iter(batch)
 
1029
        while True:
 
1030
            step = [detail for _, detail in zip(range(num), batch)]
 
1031
            if len(step) == 0:
 
1032
                break
 
1033
            yield step
269
1034
            num = min(int(num * 1.5), 200)
270
1035
 
271
 
    # now we just print all the revisions
272
 
    log_count = 0
273
 
    for ((rev_id, revno, merge_depth), (rev, delta)) in \
274
 
         izip(view_revisions, iter_revisions()):
275
 
 
276
 
        if searchRE:
277
 
            if not searchRE.search(rev.message):
278
 
                continue
279
 
 
280
 
        if not legacy_lf:
281
 
            lr = LogRevision(rev, revno, merge_depth, delta,
282
 
                             rev_tag_dict.get(rev_id))
283
 
            lf.log_revision(lr)
284
 
        else:
285
 
            # support for legacy (pre-0.17) LogFormatters
286
 
            if merge_depth == 0:
287
 
                if generate_tags:
288
 
                    lf.show(revno, rev, delta, rev_tag_dict.get(rev_id))
289
 
                else:
290
 
                    lf.show(revno, rev, delta)
291
 
            else:
292
 
                if show_merge_revno is None:
293
 
                    lf.show_merge(rev, merge_depth)
294
 
                else:
295
 
                    if generate_tags:
296
 
                        lf.show_merge_revno(rev, merge_depth, revno,
297
 
                                            rev_tag_dict.get(rev_id))
298
 
                    else:
299
 
                        lf.show_merge_revno(rev, merge_depth, revno)
300
 
        if limit:
301
 
            log_count += 1
302
 
            if log_count >= limit:
303
 
                break
 
1036
 
 
1037
def _get_revision_limits(branch, start_revision, end_revision):
 
1038
    """Get and check revision limits.
 
1039
 
 
1040
    :param  branch: The branch containing the revisions.
 
1041
 
 
1042
    :param  start_revision: The first revision to be logged.
 
1043
            For backwards compatibility this may be a mainline integer revno,
 
1044
            but for merge revision support a RevisionInfo is expected.
 
1045
 
 
1046
    :param  end_revision: The last revision to be logged.
 
1047
            For backwards compatibility this may be a mainline integer revno,
 
1048
            but for merge revision support a RevisionInfo is expected.
 
1049
 
 
1050
    :return: (start_rev_id, end_rev_id) tuple.
 
1051
    """
 
1052
    branch_revno, branch_rev_id = branch.last_revision_info()
 
1053
    start_rev_id = None
 
1054
    if start_revision is None:
 
1055
        start_revno = 1
 
1056
    else:
 
1057
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1058
            start_rev_id = start_revision.rev_id
 
1059
            start_revno = start_revision.revno or 1
 
1060
        else:
 
1061
            branch.check_real_revno(start_revision)
 
1062
            start_revno = start_revision
 
1063
            start_rev_id = branch.get_rev_id(start_revno)
 
1064
 
 
1065
    end_rev_id = None
 
1066
    if end_revision is None:
 
1067
        end_revno = branch_revno
 
1068
    else:
 
1069
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1070
            end_rev_id = end_revision.rev_id
 
1071
            end_revno = end_revision.revno or branch_revno
 
1072
        else:
 
1073
            branch.check_real_revno(end_revision)
 
1074
            end_revno = end_revision
 
1075
            end_rev_id = branch.get_rev_id(end_revno)
 
1076
 
 
1077
    if branch_revno != 0:
 
1078
        if (start_rev_id == _mod_revision.NULL_REVISION
 
1079
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1080
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1081
        if start_revno > end_revno:
 
1082
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1083
                                         "older than the end revision."))
 
1084
    return (start_rev_id, end_rev_id)
304
1085
 
305
1086
 
306
1087
def _get_mainline_revs(branch, start_revision, end_revision):
307
1088
    """Get the mainline revisions from the branch.
308
 
    
 
1089
 
309
1090
    Generates the list of mainline revisions for the branch.
310
 
    
311
 
    :param  branch: The branch containing the revisions. 
 
1091
 
 
1092
    :param  branch: The branch containing the revisions.
312
1093
 
313
1094
    :param  start_revision: The first revision to be logged.
314
1095
            For backwards compatibility this may be a mainline integer revno,
320
1101
 
321
1102
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
322
1103
    """
323
 
    which_revs = _enumerate_history(branch)
324
 
    if not which_revs:
 
1104
    branch_revno, branch_last_revision = branch.last_revision_info()
 
1105
    if branch_revno == 0:
325
1106
        return None, None, None, None
326
1107
 
327
 
    # For mainline generation, map start_revision and end_revision to 
328
 
    # mainline revnos. If the revision is not on the mainline choose the 
329
 
    # appropriate extreme of the mainline instead - the extra will be 
 
1108
    # For mainline generation, map start_revision and end_revision to
 
1109
    # mainline revnos. If the revision is not on the mainline choose the
 
1110
    # appropriate extreme of the mainline instead - the extra will be
330
1111
    # filtered later.
331
1112
    # Also map the revisions to rev_ids, to be used in the later filtering
332
1113
    # stage.
333
 
    start_rev_id = None 
 
1114
    start_rev_id = None
334
1115
    if start_revision is None:
335
1116
        start_revno = 1
336
1117
    else:
337
 
        if isinstance(start_revision,RevisionInfo):
 
1118
        if isinstance(start_revision, revisionspec.RevisionInfo):
338
1119
            start_rev_id = start_revision.rev_id
339
1120
            start_revno = start_revision.revno or 1
340
1121
        else:
341
1122
            branch.check_real_revno(start_revision)
342
1123
            start_revno = start_revision
343
 
    
 
1124
 
344
1125
    end_rev_id = None
345
1126
    if end_revision is None:
346
 
        end_revno = len(which_revs)
 
1127
        end_revno = branch_revno
347
1128
    else:
348
 
        if isinstance(end_revision,RevisionInfo):
 
1129
        if isinstance(end_revision, revisionspec.RevisionInfo):
349
1130
            end_rev_id = end_revision.rev_id
350
 
            end_revno = end_revision.revno or len(which_revs)
 
1131
            end_revno = end_revision.revno or branch_revno
351
1132
        else:
352
1133
            branch.check_real_revno(end_revision)
353
1134
            end_revno = end_revision
354
1135
 
 
1136
    if ((start_rev_id == _mod_revision.NULL_REVISION)
 
1137
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1138
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
355
1139
    if start_revno > end_revno:
356
 
        from bzrlib.errors import BzrCommandError
357
 
        raise BzrCommandError("Start revision must be older than "
358
 
                              "the end revision.")
 
1140
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1141
                                     "than the end revision."))
359
1142
 
360
 
    # list indexes are 0-based; revisions are 1-based
361
 
    cut_revs = which_revs[(start_revno-1):(end_revno)]
362
 
    if not cut_revs:
 
1143
    if end_revno < start_revno:
363
1144
        return None, None, None, None
 
1145
    cur_revno = branch_revno
 
1146
    rev_nos = {}
 
1147
    mainline_revs = []
 
1148
    graph = branch.repository.get_graph()
 
1149
    for revision_id in graph.iter_lefthand_ancestry(
 
1150
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1151
        if cur_revno < start_revno:
 
1152
            # We have gone far enough, but we always add 1 more revision
 
1153
            rev_nos[revision_id] = cur_revno
 
1154
            mainline_revs.append(revision_id)
 
1155
            break
 
1156
        if cur_revno <= end_revno:
 
1157
            rev_nos[revision_id] = cur_revno
 
1158
            mainline_revs.append(revision_id)
 
1159
        cur_revno -= 1
 
1160
    else:
 
1161
        # We walked off the edge of all revisions, so we add a 'None' marker
 
1162
        mainline_revs.append(None)
364
1163
 
365
 
    # convert the revision history to a dictionary:
366
 
    rev_nos = dict((k, v) for v, k in cut_revs)
 
1164
    mainline_revs.reverse()
367
1165
 
368
1166
    # override the mainline to look like the revision history.
369
 
    mainline_revs = [revision_id for index, revision_id in cut_revs]
370
 
    if cut_revs[0][0] == 1:
371
 
        mainline_revs.insert(0, None)
372
 
    else:
373
 
        mainline_revs.insert(0, which_revs[start_revno-2][1])
374
1167
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
375
1168
 
376
1169
 
377
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
378
 
    """Filter view_revisions based on revision ranges.
379
 
 
380
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth) 
381
 
            tuples to be filtered.
382
 
 
383
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
384
 
            If NONE then all revisions up to the end_rev_id are logged.
385
 
 
386
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
387
 
            If NONE then all revisions up to the end of the log are logged.
388
 
 
389
 
    :return: The filtered view_revisions.
390
 
    """
391
 
    if start_rev_id or end_rev_id: 
392
 
        revision_ids = [r for r, n, d in view_revisions]
393
 
        if start_rev_id:
394
 
            start_index = revision_ids.index(start_rev_id)
395
 
        else:
396
 
            start_index = 0
397
 
        if start_rev_id == end_rev_id:
398
 
            end_index = start_index
399
 
        else:
400
 
            if end_rev_id:
401
 
                end_index = revision_ids.index(end_rev_id)
402
 
            else:
403
 
                end_index = len(view_revisions) - 1
404
 
        # To include the revisions merged into the last revision, 
405
 
        # extend end_rev_id down to, but not including, the next rev
406
 
        # with the same or lesser merge_depth
407
 
        end_merge_depth = view_revisions[end_index][2]
408
 
        try:
409
 
            for index in xrange(end_index+1, len(view_revisions)+1):
410
 
                if view_revisions[index][2] <= end_merge_depth:
411
 
                    end_index = index - 1
412
 
                    break
413
 
        except IndexError:
414
 
            # if the search falls off the end then log to the end as well
415
 
            end_index = len(view_revisions) - 1
416
 
        view_revisions = view_revisions[start_index:end_index+1]
417
 
    return view_revisions
418
 
 
419
 
 
420
 
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
421
 
                                       view_revs_iter):
422
 
    """Return the list of revision ids which touch a given file id.
 
1170
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1171
    include_merges=True):
 
1172
    r"""Return the list of revision ids which touch a given file id.
423
1173
 
424
1174
    The function filters view_revisions and returns a subset.
425
1175
    This includes the revisions which directly change the file id,
426
1176
    and the revisions which merge these changes. So if the
427
1177
    revision graph is::
428
 
        A
429
 
        |\
430
 
        B C
 
1178
 
 
1179
        A-.
 
1180
        |\ \
 
1181
        B C E
 
1182
        |/ /
 
1183
        D |
 
1184
        |\|
 
1185
        | F
431
1186
        |/
432
 
        D
433
 
 
434
 
    And 'C' changes a file, then both C and D will be returned.
435
 
 
436
 
    This will also can be restricted based on a subset of the mainline.
 
1187
        G
 
1188
 
 
1189
    And 'C' changes a file, then both C and D will be returned. F will not be
 
1190
    returned even though it brings the changes to C into the branch starting
 
1191
    with E. (Note that if we were using F as the tip instead of G, then we
 
1192
    would see C, D, F.)
 
1193
 
 
1194
    This will also be restricted based on a subset of the mainline.
 
1195
 
 
1196
    :param branch: The branch where we can get text revision information.
 
1197
 
 
1198
    :param file_id: Filter out revisions that do not touch file_id.
 
1199
 
 
1200
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1201
        tuples. This is the list of revisions which will be filtered. It is
 
1202
        assumed that view_revisions is in merge_sort order (i.e. newest
 
1203
        revision first ).
 
1204
 
 
1205
    :param include_merges: include merge revisions in the result or not
437
1206
 
438
1207
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
439
1208
    """
440
 
    # find all the revisions that change the specific file
441
 
    file_weave = branch.repository.weave_store.get_weave(file_id,
442
 
                branch.repository.get_transaction())
443
 
    weave_modifed_revisions = set(file_weave.versions())
444
 
    # build the ancestry of each revision in the graph
445
 
    # - only listing the ancestors that change the specific file.
446
 
    rev_graph = branch.repository.get_revision_graph(mainline_revisions[-1])
447
 
    sorted_rev_list = topo_sort(rev_graph)
448
 
    ancestry = {}
449
 
    for rev in sorted_rev_list:
450
 
        parents = rev_graph[rev]
451
 
        if rev not in weave_modifed_revisions and len(parents) == 1:
452
 
            # We will not be adding anything new, so just use a reference to
453
 
            # the parent ancestry.
454
 
            rev_ancestry = ancestry[parents[0]]
 
1209
    # Lookup all possible text keys to determine which ones actually modified
 
1210
    # the file.
 
1211
    graph = branch.repository.get_file_graph()
 
1212
    get_parent_map = graph.get_parent_map
 
1213
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
 
1214
    next_keys = None
 
1215
    # Looking up keys in batches of 1000 can cut the time in half, as well as
 
1216
    # memory consumption. GraphIndex *does* like to look for a few keys in
 
1217
    # parallel, it just doesn't like looking for *lots* of keys in parallel.
 
1218
    # TODO: This code needs to be re-evaluated periodically as we tune the
 
1219
    #       indexing layer. We might consider passing in hints as to the known
 
1220
    #       access pattern (sparse/clustered, high success rate/low success
 
1221
    #       rate). This particular access is clustered with a low success rate.
 
1222
    modified_text_revisions = set()
 
1223
    chunk_size = 1000
 
1224
    for start in xrange(0, len(text_keys), chunk_size):
 
1225
        next_keys = text_keys[start:start + chunk_size]
 
1226
        # Only keep the revision_id portion of the key
 
1227
        modified_text_revisions.update(
 
1228
            [k[1] for k in get_parent_map(next_keys)])
 
1229
    del text_keys, next_keys
 
1230
 
 
1231
    result = []
 
1232
    # Track what revisions will merge the current revision, replace entries
 
1233
    # with 'None' when they have been added to result
 
1234
    current_merge_stack = [None]
 
1235
    for info in view_revisions:
 
1236
        rev_id, revno, depth = info
 
1237
        if depth == len(current_merge_stack):
 
1238
            current_merge_stack.append(info)
455
1239
        else:
456
 
            rev_ancestry = set()
457
 
            if rev in weave_modifed_revisions:
458
 
                rev_ancestry.add(rev)
459
 
            for parent in parents:
460
 
                rev_ancestry = rev_ancestry.union(ancestry[parent])
461
 
        ancestry[rev] = rev_ancestry
462
 
 
463
 
    def is_merging_rev(r):
464
 
        parents = rev_graph[r]
465
 
        if len(parents) > 1:
466
 
            leftparent = parents[0]
467
 
            for rightparent in parents[1:]:
468
 
                if not ancestry[leftparent].issuperset(
469
 
                        ancestry[rightparent]):
470
 
                    return True
471
 
        return False
472
 
 
473
 
    # filter from the view the revisions that did not change or merge 
474
 
    # the specific file
475
 
    return [(r, n, d) for r, n, d in view_revs_iter
476
 
            if r in weave_modifed_revisions or is_merging_rev(r)]
477
 
 
478
 
 
479
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
480
 
                       include_merges=True):
481
 
    """Produce an iterator of revisions to show
482
 
    :return: an iterator of (revision_id, revno, merge_depth)
483
 
    (if there is no revno for a revision, None is supplied)
484
 
    """
485
 
    if include_merges is False:
486
 
        revision_ids = mainline_revs[1:]
487
 
        if direction == 'reverse':
488
 
            revision_ids.reverse()
489
 
        for revision_id in revision_ids:
490
 
            yield revision_id, str(rev_nos[revision_id]), 0
491
 
        return
492
 
    merge_sorted_revisions = merge_sort(
493
 
        branch.repository.get_revision_graph(mainline_revs[-1]),
494
 
        mainline_revs[-1],
495
 
        mainline_revs,
496
 
        generate_revno=True)
497
 
 
498
 
    if direction == 'forward':
499
 
        # forward means oldest first.
500
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
501
 
    elif direction != 'reverse':
502
 
        raise ValueError('invalid direction %r' % direction)
503
 
 
504
 
    for sequence, rev_id, merge_depth, revno, end_of_merge in merge_sorted_revisions:
505
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
 
1240
            del current_merge_stack[depth + 1:]
 
1241
            current_merge_stack[-1] = info
 
1242
 
 
1243
        if rev_id in modified_text_revisions:
 
1244
            # This needs to be logged, along with the extra revisions
 
1245
            for idx in xrange(len(current_merge_stack)):
 
1246
                node = current_merge_stack[idx]
 
1247
                if node is not None:
 
1248
                    if include_merges or node[2] == 0:
 
1249
                        result.append(node)
 
1250
                        current_merge_stack[idx] = None
 
1251
    return result
506
1252
 
507
1253
 
508
1254
def reverse_by_depth(merge_sorted_revisions, _depth=0):
512
1258
    revision of that depth.  There may be no topological justification for this,
513
1259
    but it looks much nicer.
514
1260
    """
 
1261
    # Add a fake revision at start so that we can always attach sub revisions
 
1262
    merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
515
1263
    zd_revisions = []
516
1264
    for val in merge_sorted_revisions:
517
1265
        if val[2] == _depth:
 
1266
            # Each revision at the current depth becomes a chunk grouping all
 
1267
            # higher depth revisions.
518
1268
            zd_revisions.append([val])
519
1269
        else:
520
 
            assert val[2] > _depth
521
1270
            zd_revisions[-1].append(val)
522
1271
    for revisions in zd_revisions:
523
1272
        if len(revisions) > 1:
 
1273
            # We have higher depth revisions, let reverse them locally
524
1274
            revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
525
1275
    zd_revisions.reverse()
526
1276
    result = []
527
1277
    for chunk in zd_revisions:
528
1278
        result.extend(chunk)
 
1279
    if _depth == 0:
 
1280
        # Top level call, get rid of the fake revisions that have been added
 
1281
        result = [r for r in result if r[0] is not None and r[1] is not None]
529
1282
    return result
530
1283
 
531
1284
 
533
1286
    """A revision to be logged (by LogFormatter.log_revision).
534
1287
 
535
1288
    A simple wrapper for the attributes of a revision to be logged.
536
 
    The attributes may or may not be populated, as determined by the 
 
1289
    The attributes may or may not be populated, as determined by the
537
1290
    logging options and the log formatter capabilities.
538
1291
    """
539
1292
 
540
1293
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
541
 
                 tags=None):
 
1294
                 tags=None, diff=None, signature=None):
542
1295
        self.rev = rev
543
 
        self.revno = revno
 
1296
        if revno is None:
 
1297
            self.revno = None
 
1298
        else:
 
1299
            self.revno = str(revno)
544
1300
        self.merge_depth = merge_depth
545
1301
        self.delta = delta
546
1302
        self.tags = tags
 
1303
        self.diff = diff
 
1304
        self.signature = signature
547
1305
 
548
1306
 
549
1307
class LogFormatter(object):
554
1312
    If the LogFormatter needs to be informed of the beginning or end of
555
1313
    a log it should implement the begin_log and/or end_log hook methods.
556
1314
 
557
 
    A LogFormatter should define the following supports_XXX flags 
 
1315
    A LogFormatter should define the following supports_XXX flags
558
1316
    to indicate which LogRevision attributes it supports:
559
1317
 
560
1318
    - supports_delta must be True if this log formatter supports delta.
561
 
        Otherwise the delta attribute may not be populated.
562
 
    - supports_merge_revisions must be True if this log formatter supports 
563
 
        merge revisions.  If not, only revisions mainline revisions (those 
564
 
        with merge_depth == 0) will be passed to the formatter.
 
1319
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1320
      attribute describes whether the 'short_status' format (1) or the long
 
1321
      one (2) should be used.
 
1322
 
 
1323
    - supports_merge_revisions must be True if this log formatter supports
 
1324
      merge revisions.  If not, then only mainline revisions will be passed
 
1325
      to the formatter.
 
1326
 
 
1327
    - preferred_levels is the number of levels this formatter defaults to.
 
1328
      The default value is zero meaning display all levels.
 
1329
      This value is only relevant if supports_merge_revisions is True.
 
1330
 
565
1331
    - supports_tags must be True if this log formatter supports tags.
566
 
        Otherwise the tags attribute may not be populated.
 
1332
      Otherwise the tags attribute may not be populated.
 
1333
 
 
1334
    - supports_diff must be True if this log formatter supports diffs.
 
1335
      Otherwise the diff attribute may not be populated.
 
1336
 
 
1337
    - supports_signatures must be True if this log formatter supports GPG
 
1338
      signatures.
 
1339
 
 
1340
    Plugins can register functions to show custom revision properties using
 
1341
    the properties_handler_registry. The registered function
 
1342
    must respect the following interface description::
 
1343
 
 
1344
        def my_show_properties(properties_dict):
 
1345
            # code that returns a dict {'name':'value'} of the properties
 
1346
            # to be shown
567
1347
    """
568
 
 
569
 
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
 
1348
    preferred_levels = 0
 
1349
 
 
1350
    def __init__(self, to_file, show_ids=False, show_timezone='original',
 
1351
                 delta_format=None, levels=None, show_advice=False,
 
1352
                 to_exact_file=None, author_list_handler=None):
 
1353
        """Create a LogFormatter.
 
1354
 
 
1355
        :param to_file: the file to output to
 
1356
        :param to_exact_file: if set, gives an output stream to which
 
1357
             non-Unicode diffs are written.
 
1358
        :param show_ids: if True, revision-ids are to be displayed
 
1359
        :param show_timezone: the timezone to use
 
1360
        :param delta_format: the level of delta information to display
 
1361
          or None to leave it to the formatter to decide
 
1362
        :param levels: the number of levels to display; None or -1 to
 
1363
          let the log formatter decide.
 
1364
        :param show_advice: whether to show advice at the end of the
 
1365
          log or not
 
1366
        :param author_list_handler: callable generating a list of
 
1367
          authors to display for a given revision
 
1368
        """
570
1369
        self.to_file = to_file
 
1370
        # 'exact' stream used to show diff, it should print content 'as is'
 
1371
        # and should not try to decode/encode it to unicode to avoid bug #328007
 
1372
        if to_exact_file is not None:
 
1373
            self.to_exact_file = to_exact_file
 
1374
        else:
 
1375
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1376
            # for code that expects to get diffs to pass in the exact file
 
1377
            # stream
 
1378
            self.to_exact_file = getattr(to_file, 'stream', to_file)
571
1379
        self.show_ids = show_ids
572
1380
        self.show_timezone = show_timezone
573
 
 
574
 
# TODO: uncomment this block after show() has been removed.
575
 
# Until then defining log_revision would prevent _show_log calling show() 
576
 
# in legacy formatters.
577
 
#    def log_revision(self, revision):
578
 
#        """Log a revision.
579
 
#
580
 
#        :param  revision:   The LogRevision to be logged.
581
 
#        """
582
 
#        raise NotImplementedError('not implemented in abstract base')
583
 
 
584
 
    @deprecated_method(zero_seventeen)
585
 
    def show(self, revno, rev, delta):
 
1381
        if delta_format is None:
 
1382
            # Ensures backward compatibility
 
1383
            delta_format = 2 # long format
 
1384
        self.delta_format = delta_format
 
1385
        self.levels = levels
 
1386
        self._show_advice = show_advice
 
1387
        self._merge_count = 0
 
1388
        self._author_list_handler = author_list_handler
 
1389
 
 
1390
    def get_levels(self):
 
1391
        """Get the number of levels to display or 0 for all."""
 
1392
        if getattr(self, 'supports_merge_revisions', False):
 
1393
            if self.levels is None or self.levels == -1:
 
1394
                self.levels = self.preferred_levels
 
1395
        else:
 
1396
            self.levels = 1
 
1397
        return self.levels
 
1398
 
 
1399
    def log_revision(self, revision):
 
1400
        """Log a revision.
 
1401
 
 
1402
        :param  revision:   The LogRevision to be logged.
 
1403
        """
586
1404
        raise NotImplementedError('not implemented in abstract base')
587
1405
 
 
1406
    def show_advice(self):
 
1407
        """Output user advice, if any, when the log is completed."""
 
1408
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1409
            advice_sep = self.get_advice_separator()
 
1410
            if advice_sep:
 
1411
                self.to_file.write(advice_sep)
 
1412
            self.to_file.write(
 
1413
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1414
 
 
1415
    def get_advice_separator(self):
 
1416
        """Get the text separating the log from the closing advice."""
 
1417
        return ''
 
1418
 
588
1419
    def short_committer(self, rev):
589
 
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
1420
        name, address = config.parse_username(rev.committer)
 
1421
        if name:
 
1422
            return name
 
1423
        return address
 
1424
 
 
1425
    def short_author(self, rev):
 
1426
        return self.authors(rev, 'first', short=True, sep=', ')
 
1427
 
 
1428
    def authors(self, rev, who, short=False, sep=None):
 
1429
        """Generate list of authors, taking --authors option into account.
 
1430
 
 
1431
        The caller has to specify the name of a author list handler,
 
1432
        as provided by the author list registry, using the ``who``
 
1433
        argument.  That name only sets a default, though: when the
 
1434
        user selected a different author list generation using the
 
1435
        ``--authors`` command line switch, as represented by the
 
1436
        ``author_list_handler`` constructor argument, that value takes
 
1437
        precedence.
 
1438
 
 
1439
        :param rev: The revision for which to generate the list of authors.
 
1440
        :param who: Name of the default handler.
 
1441
        :param short: Whether to shorten names to either name or address.
 
1442
        :param sep: What separator to use for automatic concatenation.
 
1443
        """
 
1444
        if self._author_list_handler is not None:
 
1445
            # The user did specify --authors, which overrides the default
 
1446
            author_list_handler = self._author_list_handler
 
1447
        else:
 
1448
            # The user didn't specify --authors, so we use the caller's default
 
1449
            author_list_handler = author_list_registry.get(who)
 
1450
        names = author_list_handler(rev)
 
1451
        if short:
 
1452
            for i in range(len(names)):
 
1453
                name, address = config.parse_username(names[i])
 
1454
                if name:
 
1455
                    names[i] = name
 
1456
                else:
 
1457
                    names[i] = address
 
1458
        if sep is not None:
 
1459
            names = sep.join(names)
 
1460
        return names
 
1461
 
 
1462
    def merge_marker(self, revision):
 
1463
        """Get the merge marker to include in the output or '' if none."""
 
1464
        if len(revision.rev.parent_ids) > 1:
 
1465
            self._merge_count += 1
 
1466
            return ' [merge]'
 
1467
        else:
 
1468
            return ''
 
1469
 
 
1470
    def show_properties(self, revision, indent):
 
1471
        """Displays the custom properties returned by each registered handler.
 
1472
 
 
1473
        If a registered handler raises an error it is propagated.
 
1474
        """
 
1475
        for line in self.custom_properties(revision):
 
1476
            self.to_file.write("%s%s\n" % (indent, line))
 
1477
 
 
1478
    def custom_properties(self, revision):
 
1479
        """Format the custom properties returned by each registered handler.
 
1480
 
 
1481
        If a registered handler raises an error it is propagated.
 
1482
 
 
1483
        :return: a list of formatted lines (excluding trailing newlines)
 
1484
        """
 
1485
        lines = self._foreign_info_properties(revision)
 
1486
        for key, handler in properties_handler_registry.iteritems():
 
1487
            lines.extend(self._format_properties(handler(revision)))
 
1488
        return lines
 
1489
 
 
1490
    def _foreign_info_properties(self, rev):
 
1491
        """Custom log displayer for foreign revision identifiers.
 
1492
 
 
1493
        :param rev: Revision object.
 
1494
        """
 
1495
        # Revision comes directly from a foreign repository
 
1496
        if isinstance(rev, foreign.ForeignRevision):
 
1497
            return self._format_properties(
 
1498
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1499
 
 
1500
        # Imported foreign revision revision ids always contain :
 
1501
        if not ":" in rev.revision_id:
 
1502
            return []
 
1503
 
 
1504
        # Revision was once imported from a foreign repository
 
1505
        try:
 
1506
            foreign_revid, mapping = \
 
1507
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1508
        except errors.InvalidRevisionId:
 
1509
            return []
 
1510
 
 
1511
        return self._format_properties(
 
1512
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1513
 
 
1514
    def _format_properties(self, properties):
 
1515
        lines = []
 
1516
        for key, value in properties.items():
 
1517
            lines.append(key + ': ' + value)
 
1518
        return lines
 
1519
 
 
1520
    def show_diff(self, to_file, diff, indent):
 
1521
        for l in diff.rstrip().split('\n'):
 
1522
            to_file.write(indent + '%s\n' % (l,))
 
1523
 
 
1524
 
 
1525
# Separator between revisions in long format
 
1526
_LONG_SEP = '-' * 60
590
1527
 
591
1528
 
592
1529
class LongLogFormatter(LogFormatter):
593
1530
 
594
1531
    supports_merge_revisions = True
 
1532
    preferred_levels = 1
595
1533
    supports_delta = True
596
1534
    supports_tags = True
597
 
 
598
 
    @deprecated_method(zero_seventeen)
599
 
    def show(self, revno, rev, delta, tags=None):
600
 
        lr = LogRevision(rev, revno, 0, delta, tags)
601
 
        return self.log_revision(lr)
602
 
 
603
 
    @deprecated_method(zero_eleven)
604
 
    def show_merge(self, rev, merge_depth):
605
 
        lr = LogRevision(rev, merge_depth=merge_depth)
606
 
        return self.log_revision(lr)
607
 
 
608
 
    @deprecated_method(zero_seventeen)
609
 
    def show_merge_revno(self, rev, merge_depth, revno, tags=None):
610
 
        """Show a merged revision rev, with merge_depth and a revno."""
611
 
        lr = LogRevision(rev, revno, merge_depth, tags=tags)
612
 
        return self.log_revision(lr)
 
1535
    supports_diff = True
 
1536
    supports_signatures = True
 
1537
 
 
1538
    def __init__(self, *args, **kwargs):
 
1539
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1540
        if self.show_timezone == 'original':
 
1541
            self.date_string = self._date_string_original_timezone
 
1542
        else:
 
1543
            self.date_string = self._date_string_with_timezone
 
1544
 
 
1545
    def _date_string_with_timezone(self, rev):
 
1546
        return format_date(rev.timestamp, rev.timezone or 0,
 
1547
                           self.show_timezone)
 
1548
 
 
1549
    def _date_string_original_timezone(self, rev):
 
1550
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1551
            rev.timezone or 0)
613
1552
 
614
1553
    def log_revision(self, revision):
615
1554
        """Log a revision, either merged or not."""
616
 
        from bzrlib.osutils import format_date
617
 
        indent = '    '*revision.merge_depth
618
 
        to_file = self.to_file
619
 
        print >>to_file,  indent+'-' * 60
 
1555
        indent = '    ' * revision.merge_depth
 
1556
        lines = [_LONG_SEP]
620
1557
        if revision.revno is not None:
621
 
            print >>to_file,  indent+'revno:', revision.revno
 
1558
            lines.append('revno: %s%s' % (revision.revno,
 
1559
                self.merge_marker(revision)))
622
1560
        if revision.tags:
623
 
            print >>to_file, indent+'tags: %s' % (', '.join(revision.tags))
 
1561
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1562
        if self.show_ids or revision.revno is None:
 
1563
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
624
1564
        if self.show_ids:
625
 
            print >>to_file, indent+'revision-id:', revision.rev.revision_id
626
1565
            for parent_id in revision.rev.parent_ids:
627
 
                print >>to_file, indent+'parent:', parent_id
628
 
        print >>to_file, indent+'committer:', revision.rev.committer
629
 
 
630
 
        try:
631
 
            print >>to_file, indent+'branch nick: %s' % \
632
 
                revision.rev.properties['branch-nick']
633
 
        except KeyError:
634
 
            pass
635
 
        date_str = format_date(revision.rev.timestamp,
636
 
                               revision.rev.timezone or 0,
637
 
                               self.show_timezone)
638
 
        print >>to_file,  indent+'timestamp: %s' % date_str
639
 
 
640
 
        print >>to_file,  indent+'message:'
 
1566
                lines.append('parent: %s' % (parent_id,))
 
1567
        lines.extend(self.custom_properties(revision.rev))
 
1568
 
 
1569
        committer = revision.rev.committer
 
1570
        authors = self.authors(revision.rev, 'all')
 
1571
        if authors != [committer]:
 
1572
            lines.append('author: %s' % (", ".join(authors),))
 
1573
        lines.append('committer: %s' % (committer,))
 
1574
 
 
1575
        branch_nick = revision.rev.properties.get('branch-nick', None)
 
1576
        if branch_nick is not None:
 
1577
            lines.append('branch nick: %s' % (branch_nick,))
 
1578
 
 
1579
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1580
 
 
1581
        if revision.signature is not None:
 
1582
            lines.append('signature: ' + revision.signature)
 
1583
 
 
1584
        lines.append('message:')
641
1585
        if not revision.rev.message:
642
 
            print >>to_file,  indent+'  (no message)'
 
1586
            lines.append('  (no message)')
643
1587
        else:
644
1588
            message = revision.rev.message.rstrip('\r\n')
645
1589
            for l in message.split('\n'):
646
 
                print >>to_file,  indent+'  ' + l
 
1590
                lines.append('  %s' % (l,))
 
1591
 
 
1592
        # Dump the output, appending the delta and diff if requested
 
1593
        to_file = self.to_file
 
1594
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
647
1595
        if revision.delta is not None:
648
 
            revision.delta.show(to_file, self.show_ids, indent=indent)
 
1596
            # Use the standard status output to display changes
 
1597
            from bzrlib.delta import report_delta
 
1598
            report_delta(to_file, revision.delta, short_status=False,
 
1599
                         show_ids=self.show_ids, indent=indent)
 
1600
        if revision.diff is not None:
 
1601
            to_file.write(indent + 'diff:\n')
 
1602
            to_file.flush()
 
1603
            # Note: we explicitly don't indent the diff (relative to the
 
1604
            # revision information) so that the output can be fed to patch -p0
 
1605
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1606
            self.to_exact_file.flush()
 
1607
 
 
1608
    def get_advice_separator(self):
 
1609
        """Get the text separating the log from the closing advice."""
 
1610
        return '-' * 60 + '\n'
649
1611
 
650
1612
 
651
1613
class ShortLogFormatter(LogFormatter):
652
1614
 
 
1615
    supports_merge_revisions = True
 
1616
    preferred_levels = 1
653
1617
    supports_delta = True
 
1618
    supports_tags = True
 
1619
    supports_diff = True
654
1620
 
655
 
    @deprecated_method(zero_seventeen)
656
 
    def show(self, revno, rev, delta):
657
 
        lr = LogRevision(rev, revno, 0, delta)
658
 
        return self.log_revision(lr)
 
1621
    def __init__(self, *args, **kwargs):
 
1622
        super(ShortLogFormatter, self).__init__(*args, **kwargs)
 
1623
        self.revno_width_by_depth = {}
659
1624
 
660
1625
    def log_revision(self, revision):
661
 
        from bzrlib.osutils import format_date
 
1626
        # We need two indents: one per depth and one for the information
 
1627
        # relative to that indent. Most mainline revnos are 5 chars or
 
1628
        # less while dotted revnos are typically 11 chars or less. Once
 
1629
        # calculated, we need to remember the offset for a given depth
 
1630
        # as we might be starting from a dotted revno in the first column
 
1631
        # and we want subsequent mainline revisions to line up.
 
1632
        depth = revision.merge_depth
 
1633
        indent = '    ' * depth
 
1634
        revno_width = self.revno_width_by_depth.get(depth)
 
1635
        if revno_width is None:
 
1636
            if revision.revno is None or revision.revno.find('.') == -1:
 
1637
                # mainline revno, e.g. 12345
 
1638
                revno_width = 5
 
1639
            else:
 
1640
                # dotted revno, e.g. 12345.10.55
 
1641
                revno_width = 11
 
1642
            self.revno_width_by_depth[depth] = revno_width
 
1643
        offset = ' ' * (revno_width + 1)
662
1644
 
663
1645
        to_file = self.to_file
664
 
        date_str = format_date(revision.rev.timestamp,
665
 
                               revision.rev.timezone or 0,
666
 
                               self.show_timezone)
667
 
        is_merge = ''
668
 
        if len(revision.rev.parent_ids) > 1:
669
 
            is_merge = ' [merge]'
670
 
        print >>to_file, "%5s %s\t%s%s" % (revision.revno,
671
 
                self.short_committer(revision.rev),
 
1646
        tags = ''
 
1647
        if revision.tags:
 
1648
            tags = ' {%s}' % (', '.join(revision.tags))
 
1649
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
 
1650
                revision.revno or "", self.short_author(revision.rev),
672
1651
                format_date(revision.rev.timestamp,
673
1652
                            revision.rev.timezone or 0,
674
1653
                            self.show_timezone, date_fmt="%Y-%m-%d",
675
1654
                            show_offset=False),
676
 
                is_merge)
677
 
        if self.show_ids:
678
 
            print >>to_file,  '      revision-id:', revision.rev.revision_id
 
1655
                tags, self.merge_marker(revision)))
 
1656
        self.show_properties(revision.rev, indent+offset)
 
1657
        if self.show_ids or revision.revno is None:
 
1658
            to_file.write(indent + offset + 'revision-id:%s\n'
 
1659
                          % (revision.rev.revision_id,))
679
1660
        if not revision.rev.message:
680
 
            print >>to_file,  '      (no message)'
 
1661
            to_file.write(indent + offset + '(no message)\n')
681
1662
        else:
682
1663
            message = revision.rev.message.rstrip('\r\n')
683
1664
            for l in message.split('\n'):
684
 
                print >>to_file,  '      ' + l
 
1665
                to_file.write(indent + offset + '%s\n' % (l,))
685
1666
 
686
 
        # TODO: Why not show the modified files in a shorter form as
687
 
        # well? rewrap them single lines of appropriate length
688
1667
        if revision.delta is not None:
689
 
            revision.delta.show(to_file, self.show_ids)
690
 
        print >>to_file, ''
 
1668
            # Use the standard status output to display changes
 
1669
            from bzrlib.delta import report_delta
 
1670
            report_delta(to_file, revision.delta,
 
1671
                         short_status=self.delta_format==1,
 
1672
                         show_ids=self.show_ids, indent=indent + offset)
 
1673
        if revision.diff is not None:
 
1674
            self.show_diff(self.to_exact_file, revision.diff, '      ')
 
1675
        to_file.write('\n')
691
1676
 
692
1677
 
693
1678
class LineLogFormatter(LogFormatter):
694
1679
 
 
1680
    supports_merge_revisions = True
 
1681
    preferred_levels = 1
 
1682
    supports_tags = True
 
1683
 
695
1684
    def __init__(self, *args, **kwargs):
696
 
        from bzrlib.osutils import terminal_width
697
1685
        super(LineLogFormatter, self).__init__(*args, **kwargs)
698
 
        self._max_chars = terminal_width() - 1
 
1686
        width = terminal_width()
 
1687
        if width is not None:
 
1688
            # we need one extra space for terminals that wrap on last char
 
1689
            width = width - 1
 
1690
        self._max_chars = width
699
1691
 
700
1692
    def truncate(self, str, max_len):
701
 
        if len(str) <= max_len:
 
1693
        if max_len is None or len(str) <= max_len:
702
1694
            return str
703
 
        return str[:max_len-3]+'...'
 
1695
        return str[:max_len-3] + '...'
704
1696
 
705
1697
    def date_string(self, rev):
706
 
        from bzrlib.osutils import format_date
707
 
        return format_date(rev.timestamp, rev.timezone or 0, 
 
1698
        return format_date(rev.timestamp, rev.timezone or 0,
708
1699
                           self.show_timezone, date_fmt="%Y-%m-%d",
709
1700
                           show_offset=False)
710
1701
 
714
1705
        else:
715
1706
            return rev.message
716
1707
 
717
 
    @deprecated_method(zero_seventeen)
718
 
    def show(self, revno, rev, delta):
719
 
        from bzrlib.osutils import terminal_width
720
 
        print >> self.to_file, self.log_string(revno, rev, terminal_width()-1)
721
 
 
722
1708
    def log_revision(self, revision):
723
 
        print >>self.to_file, self.log_string(revision.revno, revision.rev,
724
 
                                              self._max_chars)
 
1709
        indent = '  ' * revision.merge_depth
 
1710
        self.to_file.write(self.log_string(revision.revno, revision.rev,
 
1711
            self._max_chars, revision.tags, indent))
 
1712
        self.to_file.write('\n')
725
1713
 
726
 
    def log_string(self, revno, rev, max_chars):
 
1714
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
727
1715
        """Format log info into one string. Truncate tail of string
728
 
        :param  revno:      revision number (int) or None.
729
 
                            Revision numbers counts from 1.
730
 
        :param  rev:        revision info object
731
 
        :param  max_chars:  maximum length of resulting string
732
 
        :return:            formatted truncated string
 
1716
 
 
1717
        :param revno:      revision number or None.
 
1718
                           Revision numbers counts from 1.
 
1719
        :param rev:        revision object
 
1720
        :param max_chars:  maximum length of resulting string
 
1721
        :param tags:       list of tags or None
 
1722
        :param prefix:     string to prefix each line
 
1723
        :return:           formatted truncated string
733
1724
        """
734
1725
        out = []
735
1726
        if revno:
736
1727
            # show revno only when is not None
737
1728
            out.append("%s:" % revno)
738
 
        out.append(self.truncate(self.short_committer(rev), 20))
 
1729
        if max_chars is not None:
 
1730
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1731
        else:
 
1732
            out.append(self.short_author(rev))
739
1733
        out.append(self.date_string(rev))
 
1734
        if len(rev.parent_ids) > 1:
 
1735
            out.append('[merge]')
 
1736
        if tags:
 
1737
            tag_str = '{%s}' % (', '.join(tags))
 
1738
            out.append(tag_str)
740
1739
        out.append(rev.get_summary())
741
 
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
 
1740
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
 
1741
 
 
1742
 
 
1743
class GnuChangelogLogFormatter(LogFormatter):
 
1744
 
 
1745
    supports_merge_revisions = True
 
1746
    supports_delta = True
 
1747
 
 
1748
    def log_revision(self, revision):
 
1749
        """Log a revision, either merged or not."""
 
1750
        to_file = self.to_file
 
1751
 
 
1752
        date_str = format_date(revision.rev.timestamp,
 
1753
                               revision.rev.timezone or 0,
 
1754
                               self.show_timezone,
 
1755
                               date_fmt='%Y-%m-%d',
 
1756
                               show_offset=False)
 
1757
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1758
        committer_str = committer_str.replace(' <', '  <')
 
1759
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
 
1760
 
 
1761
        if revision.delta is not None and revision.delta.has_changed():
 
1762
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
 
1763
                path, = c[:1]
 
1764
                to_file.write('\t* %s:\n' % (path,))
 
1765
            for c in revision.delta.renamed:
 
1766
                oldpath,newpath = c[:2]
 
1767
                # For renamed files, show both the old and the new path
 
1768
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
 
1769
            to_file.write('\n')
 
1770
 
 
1771
        if not revision.rev.message:
 
1772
            to_file.write('\tNo commit message\n')
 
1773
        else:
 
1774
            message = revision.rev.message.rstrip('\r\n')
 
1775
            for l in message.split('\n'):
 
1776
                to_file.write('\t%s\n' % (l.lstrip(),))
 
1777
            to_file.write('\n')
742
1778
 
743
1779
 
744
1780
def line_log(rev, max_chars):
758
1794
        return self.get(name)(*args, **kwargs)
759
1795
 
760
1796
    def get_default(self, branch):
761
 
        return self.get(branch.get_config().log_format())
 
1797
        c = branch.get_config_stack()
 
1798
        return self.get(c.get('log_format'))
762
1799
 
763
1800
 
764
1801
log_formatter_registry = LogFormatterRegistry()
765
1802
 
766
1803
 
767
1804
log_formatter_registry.register('short', ShortLogFormatter,
768
 
                                'Moderately short log format')
 
1805
                                'Moderately short log format.')
769
1806
log_formatter_registry.register('long', LongLogFormatter,
770
 
                                'Detailed log format')
 
1807
                                'Detailed log format.')
771
1808
log_formatter_registry.register('line', LineLogFormatter,
772
 
                                'Log format with one line per revision')
 
1809
                                'Log format with one line per revision.')
 
1810
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
 
1811
                                'Format used by GNU ChangeLog files.')
773
1812
 
774
1813
 
775
1814
def register_formatter(name, formatter):
782
1821
    name -- Name of the formatter to construct; currently 'long', 'short' and
783
1822
        'line' are supported.
784
1823
    """
785
 
    from bzrlib.errors import BzrCommandError
786
1824
    try:
787
1825
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
788
1826
    except KeyError:
789
 
        raise BzrCommandError("unknown log formatter: %r" % name)
790
 
 
791
 
 
792
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
793
 
    # deprecated; for compatibility
794
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
795
 
    lf.show(revno, rev, delta)
796
 
 
797
 
 
798
 
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
 
1827
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1828
 
 
1829
 
 
1830
def author_list_all(rev):
 
1831
    return rev.get_apparent_authors()[:]
 
1832
 
 
1833
 
 
1834
def author_list_first(rev):
 
1835
    lst = rev.get_apparent_authors()
 
1836
    try:
 
1837
        return [lst[0]]
 
1838
    except IndexError:
 
1839
        return []
 
1840
 
 
1841
 
 
1842
def author_list_committer(rev):
 
1843
    return [rev.committer]
 
1844
 
 
1845
 
 
1846
author_list_registry = registry.Registry()
 
1847
 
 
1848
author_list_registry.register('all', author_list_all,
 
1849
                              'All authors')
 
1850
 
 
1851
author_list_registry.register('first', author_list_first,
 
1852
                              'The first author')
 
1853
 
 
1854
author_list_registry.register('committer', author_list_committer,
 
1855
                              'The committer')
 
1856
 
 
1857
 
 
1858
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
 
1859
                           log_format='long'):
799
1860
    """Show the change in revision history comparing the old revision history to the new one.
800
1861
 
801
1862
    :param branch: The branch where the revisions exist
804
1865
    :param to_file: A file to write the results to. If None, stdout will be used
805
1866
    """
806
1867
    if to_file is None:
807
 
        import sys
808
 
        import codecs
809
 
        import bzrlib
810
 
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
 
1868
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
 
1869
            errors='replace')
811
1870
    lf = log_formatter(log_format,
812
1871
                       show_ids=False,
813
1872
                       to_file=to_file,
844
1903
        show_log(branch,
845
1904
                 lf,
846
1905
                 None,
847
 
                 verbose=True,
 
1906
                 verbose=False,
848
1907
                 direction='forward',
849
1908
                 start_revision=base_idx+1,
850
1909
                 end_revision=len(new_rh),
851
1910
                 search=None)
852
1911
 
 
1912
 
 
1913
def get_history_change(old_revision_id, new_revision_id, repository):
 
1914
    """Calculate the uncommon lefthand history between two revisions.
 
1915
 
 
1916
    :param old_revision_id: The original revision id.
 
1917
    :param new_revision_id: The new revision id.
 
1918
    :param repository: The repository to use for the calculation.
 
1919
 
 
1920
    return old_history, new_history
 
1921
    """
 
1922
    old_history = []
 
1923
    old_revisions = set()
 
1924
    new_history = []
 
1925
    new_revisions = set()
 
1926
    graph = repository.get_graph()
 
1927
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1928
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
 
1929
    stop_revision = None
 
1930
    do_old = True
 
1931
    do_new = True
 
1932
    while do_new or do_old:
 
1933
        if do_new:
 
1934
            try:
 
1935
                new_revision = new_iter.next()
 
1936
            except StopIteration:
 
1937
                do_new = False
 
1938
            else:
 
1939
                new_history.append(new_revision)
 
1940
                new_revisions.add(new_revision)
 
1941
                if new_revision in old_revisions:
 
1942
                    stop_revision = new_revision
 
1943
                    break
 
1944
        if do_old:
 
1945
            try:
 
1946
                old_revision = old_iter.next()
 
1947
            except StopIteration:
 
1948
                do_old = False
 
1949
            else:
 
1950
                old_history.append(old_revision)
 
1951
                old_revisions.add(old_revision)
 
1952
                if old_revision in new_revisions:
 
1953
                    stop_revision = old_revision
 
1954
                    break
 
1955
    new_history.reverse()
 
1956
    old_history.reverse()
 
1957
    if stop_revision is not None:
 
1958
        new_history = new_history[new_history.index(stop_revision) + 1:]
 
1959
        old_history = old_history[old_history.index(stop_revision) + 1:]
 
1960
    return old_history, new_history
 
1961
 
 
1962
 
 
1963
def show_branch_change(branch, output, old_revno, old_revision_id):
 
1964
    """Show the changes made to a branch.
 
1965
 
 
1966
    :param branch: The branch to show changes about.
 
1967
    :param output: A file-like object to write changes to.
 
1968
    :param old_revno: The revno of the old tip.
 
1969
    :param old_revision_id: The revision_id of the old tip.
 
1970
    """
 
1971
    new_revno, new_revision_id = branch.last_revision_info()
 
1972
    old_history, new_history = get_history_change(old_revision_id,
 
1973
                                                  new_revision_id,
 
1974
                                                  branch.repository)
 
1975
    if old_history == [] and new_history == []:
 
1976
        output.write('Nothing seems to have changed\n')
 
1977
        return
 
1978
 
 
1979
    log_format = log_formatter_registry.get_default(branch)
 
1980
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
 
1981
    if old_history != []:
 
1982
        output.write('*'*60)
 
1983
        output.write('\nRemoved Revisions:\n')
 
1984
        show_flat_log(branch.repository, old_history, old_revno, lf)
 
1985
        output.write('*'*60)
 
1986
        output.write('\n\n')
 
1987
    if new_history != []:
 
1988
        output.write('Added Revisions:\n')
 
1989
        start_revno = new_revno - len(new_history) + 1
 
1990
        show_log(branch, lf, None, verbose=False, direction='forward',
 
1991
                 start_revision=start_revno,)
 
1992
 
 
1993
 
 
1994
def show_flat_log(repository, history, last_revno, lf):
 
1995
    """Show a simple log of the specified history.
 
1996
 
 
1997
    :param repository: The repository to retrieve revisions from.
 
1998
    :param history: A list of revision_ids indicating the lefthand history.
 
1999
    :param last_revno: The revno of the last revision_id in the history.
 
2000
    :param lf: The log formatter to use.
 
2001
    """
 
2002
    start_revno = last_revno - len(history) + 1
 
2003
    revisions = repository.get_revisions(history)
 
2004
    for i, rev in enumerate(revisions):
 
2005
        lr = LogRevision(rev, i + last_revno, 0, None)
 
2006
        lf.log_revision(lr)
 
2007
 
 
2008
 
 
2009
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2010
    """Find file-ids and kinds given a list of files and a revision range.
 
2011
 
 
2012
    We search for files at the end of the range. If not found there,
 
2013
    we try the start of the range.
 
2014
 
 
2015
    :param revisionspec_list: revision range as parsed on the command line
 
2016
    :param file_list: the list of paths given on the command line;
 
2017
      the first of these can be a branch location or a file path,
 
2018
      the remainder must be file paths
 
2019
    :param add_cleanup: When the branch returned is read locked,
 
2020
      an unlock call will be queued to the cleanup.
 
2021
    :return: (branch, info_list, start_rev_info, end_rev_info) where
 
2022
      info_list is a list of (relative_path, file_id, kind) tuples where
 
2023
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2024
      branch will be read-locked.
 
2025
    """
 
2026
    from bzrlib.builtins import _get_revision_range
 
2027
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2028
        file_list[0])
 
2029
    add_cleanup(b.lock_read().unlock)
 
2030
    # XXX: It's damn messy converting a list of paths to relative paths when
 
2031
    # those paths might be deleted ones, they might be on a case-insensitive
 
2032
    # filesystem and/or they might be in silly locations (like another branch).
 
2033
    # For example, what should "log bzr://branch/dir/file1 file2" do? (Is
 
2034
    # file2 implicitly in the same dir as file1 or should its directory be
 
2035
    # taken from the current tree somehow?) For now, this solves the common
 
2036
    # case of running log in a nested directory, assuming paths beyond the
 
2037
    # first one haven't been deleted ...
 
2038
    if tree:
 
2039
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
2040
    else:
 
2041
        relpaths = [path] + file_list[1:]
 
2042
    info_list = []
 
2043
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
 
2044
        "log")
 
2045
    if relpaths in ([], [u'']):
 
2046
        return b, [], start_rev_info, end_rev_info
 
2047
    if start_rev_info is None and end_rev_info is None:
 
2048
        if tree is None:
 
2049
            tree = b.basis_tree()
 
2050
        tree1 = None
 
2051
        for fp in relpaths:
 
2052
            file_id = tree.path2id(fp)
 
2053
            kind = _get_kind_for_file_id(tree, file_id)
 
2054
            if file_id is None:
 
2055
                # go back to when time began
 
2056
                if tree1 is None:
 
2057
                    try:
 
2058
                        rev1 = b.get_rev_id(1)
 
2059
                    except errors.NoSuchRevision:
 
2060
                        # No history at all
 
2061
                        file_id = None
 
2062
                        kind = None
 
2063
                    else:
 
2064
                        tree1 = b.repository.revision_tree(rev1)
 
2065
                if tree1:
 
2066
                    file_id = tree1.path2id(fp)
 
2067
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2068
            info_list.append((fp, file_id, kind))
 
2069
 
 
2070
    elif start_rev_info == end_rev_info:
 
2071
        # One revision given - file must exist in it
 
2072
        tree = b.repository.revision_tree(end_rev_info.rev_id)
 
2073
        for fp in relpaths:
 
2074
            file_id = tree.path2id(fp)
 
2075
            kind = _get_kind_for_file_id(tree, file_id)
 
2076
            info_list.append((fp, file_id, kind))
 
2077
 
 
2078
    else:
 
2079
        # Revision range given. Get the file-id from the end tree.
 
2080
        # If that fails, try the start tree.
 
2081
        rev_id = end_rev_info.rev_id
 
2082
        if rev_id is None:
 
2083
            tree = b.basis_tree()
 
2084
        else:
 
2085
            tree = b.repository.revision_tree(rev_id)
 
2086
        tree1 = None
 
2087
        for fp in relpaths:
 
2088
            file_id = tree.path2id(fp)
 
2089
            kind = _get_kind_for_file_id(tree, file_id)
 
2090
            if file_id is None:
 
2091
                if tree1 is None:
 
2092
                    rev_id = start_rev_info.rev_id
 
2093
                    if rev_id is None:
 
2094
                        rev1 = b.get_rev_id(1)
 
2095
                        tree1 = b.repository.revision_tree(rev1)
 
2096
                    else:
 
2097
                        tree1 = b.repository.revision_tree(rev_id)
 
2098
                file_id = tree1.path2id(fp)
 
2099
                kind = _get_kind_for_file_id(tree1, file_id)
 
2100
            info_list.append((fp, file_id, kind))
 
2101
    return b, info_list, start_rev_info, end_rev_info
 
2102
 
 
2103
 
 
2104
def _get_kind_for_file_id(tree, file_id):
 
2105
    """Return the kind of a file-id or None if it doesn't exist."""
 
2106
    if file_id is not None:
 
2107
        return tree.kind(file_id)
 
2108
    else:
 
2109
        return None
 
2110
 
 
2111
 
 
2112
properties_handler_registry = registry.Registry()
 
2113
 
 
2114
# Use the properties handlers to print out bug information if available
 
2115
def _bugs_properties_handler(revision):
 
2116
    if revision.properties.has_key('bugs'):
 
2117
        bug_lines = revision.properties['bugs'].split('\n')
 
2118
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2119
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2120
                          len(row) > 1 and row[1] == 'fixed']
 
2121
 
 
2122
        if fixed_bug_urls:
 
2123
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2124
                    ' '.join(fixed_bug_urls)}
 
2125
    return {}
 
2126
 
 
2127
properties_handler_registry.register('bugs_properties_handler',
 
2128
                                     _bugs_properties_handler)
 
2129
 
 
2130
 
 
2131
# adapters which revision ids to log are filtered. When log is called, the
 
2132
# log_rev_iterator is adapted through each of these factory methods.
 
2133
# Plugins are welcome to mutate this list in any way they like - as long
 
2134
# as the overall behaviour is preserved. At this point there is no extensible
 
2135
# mechanism for getting parameters to each factory method, and until there is
 
2136
# this won't be considered a stable api.
 
2137
log_adapters = [
 
2138
    # core log logic
 
2139
    _make_batch_filter,
 
2140
    # read revision objects
 
2141
    _make_revision_objects,
 
2142
    # filter on log messages
 
2143
    _make_search_filter,
 
2144
    # generate deltas for things we will show
 
2145
    _make_delta_filter
 
2146
    ]