/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-19 10:58:39 UTC
  • mfrom: (6383 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6386.
  • Revision ID: jelmer@canonical.com-20111219105839-uji05ck4rkm1mj4j
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

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