~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Jelmer Vernooij
  • Date: 2010-08-29 13:42:47 UTC
  • mto: This revision was merged to the branch mainline in revision 5418.
  • Revision ID: jelmer@samba.org-20100829134247-37jrn4qw7siviq51
Move clone() onto ControlDir.clone(), add ControlDir.clone_on_transport() stub.

Show diffs side-by-side

added added

removed removed

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