~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Robert Collins
  • Date: 2005-09-29 00:24:44 UTC
  • Revision ID: robertc@robertcollins.net-20050929002444-76fe66e99fb9bcd5
reinstate testfetch test case

Show diffs side-by-side

added added

removed removed

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