~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

(jelmer) Support upgrading between the 2a and development-colo formats.
 (Jelmer Vernooij)

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