/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: 2017-06-10 16:40:42 UTC
  • mfrom: (6653.6.7 rename-controldir)
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170610164042-zrxqgy2htyduvke2
MergeĀ rename-controldirĀ branch.

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