/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Ferlito
  • Date: 2009-09-02 04:31:45 UTC
  • mto: (4665.7.1 serve-init)
  • mto: This revision was merged to the branch mainline in revision 4913.
  • Revision ID: johnf@inodes.org-20090902043145-gxdsfw03ilcwbyn5
Add a debian init script for bzr --serve

Show diffs side-by-side

added added

removed removed

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