/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 breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2018-02-18 21:42:57 UTC
  • mto: This revision was merged to the branch mainline in revision 6859.
  • Revision ID: jelmer@jelmer.uk-20180218214257-jpevutp1wa30tz3v
Update TODO to reference Breezy, not Bazaar.

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