~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Launchpad Translations on behalf of bzr-core
  • Date: 2012-05-29 04:30:47 UTC
  • mto: (6581.1.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 6582.
  • Revision ID: launchpad_translations_on_behalf_of_bzr-core-20120529043047-mmj2hkq63ke8kl81
Launchpad automatic translations update.

Show diffs side-by-side

added added

removed removed

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