~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: John Arbash Meinel
  • Date: 2008-10-30 00:55:00 UTC
  • mto: (3815.2.5 prepare-1.9)
  • mto: This revision was merged to the branch mainline in revision 3811.
  • Revision ID: john@arbash-meinel.com-20081030005500-r5cej1cxflqhs3io
Switch so that we are using a simple timestamp as the first action.

Show diffs side-by-side

added added

removed removed

Lines of Context:
59
59
    warn,
60
60
    )
61
61
 
 
62
from bzrlib.lazy_import import lazy_import
 
63
lazy_import(globals(), """
 
64
 
62
65
from bzrlib import (
63
66
    config,
64
 
    lazy_regex,
 
67
    errors,
 
68
    repository as _mod_repository,
 
69
    revision as _mod_revision,
 
70
    revisionspec,
 
71
    trace,
 
72
    tsort,
 
73
    )
 
74
""")
 
75
 
 
76
from bzrlib import (
65
77
    registry,
66
78
    )
67
 
from bzrlib.errors import (
68
 
    BzrCommandError,
69
 
    )
70
79
from bzrlib.osutils import (
71
80
    format_date,
72
81
    get_terminal_encoding,
73
82
    terminal_width,
74
83
    )
75
 
from bzrlib.repository import _strip_NULL_ghosts
76
 
from bzrlib.revision import (
77
 
    NULL_REVISION,
78
 
    )
79
 
from bzrlib.revisionspec import (
80
 
    RevisionInfo,
81
 
    )
82
 
from bzrlib.trace import mutter
83
 
from bzrlib.tsort import (
84
 
    merge_sort,
85
 
    topo_sort,
86
 
    )
87
84
 
88
85
 
89
86
def find_touching_revisions(branch, file_id):
204
201
        warn("not a LogFormatter instance: %r" % lf)
205
202
 
206
203
    if specific_fileid:
207
 
        mutter('get log for file_id %r', specific_fileid)
 
204
        trace.mutter('get log for file_id %r', specific_fileid)
208
205
    generate_merge_revisions = getattr(lf, 'supports_merge_revisions', False)
209
206
    allow_single_merge_revision = getattr(lf,
210
207
        'supports_single_merge_revision', False)
213
210
                                              specific_fileid,
214
211
                                              generate_merge_revisions,
215
212
                                              allow_single_merge_revision)
216
 
    if search is not None:
217
 
        searchRE = re.compile(search, re.IGNORECASE)
218
 
    else:
219
 
        searchRE = None
220
 
 
221
213
    rev_tag_dict = {}
222
214
    generate_tags = getattr(lf, 'supports_tags', False)
223
215
    if generate_tags:
228
220
 
229
221
    # now we just print all the revisions
230
222
    log_count = 0
231
 
    for (rev_id, revno, merge_depth), rev, delta in _iter_revisions(
232
 
        branch.repository, view_revisions, generate_delta):
233
 
        if searchRE:
234
 
            if not searchRE.search(rev.message):
235
 
                continue
236
 
 
237
 
        lr = LogRevision(rev, revno, merge_depth, delta,
238
 
                         rev_tag_dict.get(rev_id))
239
 
        lf.log_revision(lr)
240
 
        if limit:
241
 
            log_count += 1
242
 
            if log_count >= limit:
243
 
                break
 
223
    revision_iterator = make_log_rev_iterator(branch, view_revisions,
 
224
        generate_delta, search)
 
225
    for revs in revision_iterator:
 
226
        for (rev_id, revno, merge_depth), rev, delta in revs:
 
227
            lr = LogRevision(rev, revno, merge_depth, delta,
 
228
                             rev_tag_dict.get(rev_id))
 
229
            lf.log_revision(lr)
 
230
            if limit:
 
231
                log_count += 1
 
232
                if log_count >= limit:
 
233
                    return
244
234
 
245
235
 
246
236
def calculate_view_revisions(branch, start_revision, end_revision, direction,
265
255
        generate_single_revision = ((start_rev_id == end_rev_id)
266
256
            and allow_single_merge_revision)
267
257
        if not generate_single_revision:
268
 
            raise BzrCommandError('Selected log formatter only supports '
269
 
                'mainline revisions.')
 
258
            raise errors.BzrCommandError('Selected log formatter only supports'
 
259
                ' mainline revisions.')
270
260
        generate_merge_revisions = generate_single_revision
271
261
    view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
272
262
                          direction, include_merges=generate_merge_revisions)
278
268
    if specific_fileid:
279
269
        view_revisions = _filter_revisions_touching_file_id(branch,
280
270
                                                         specific_fileid,
281
 
                                                         mainline_revs,
282
 
                                                         view_revisions)
 
271
                                                         view_revisions,
 
272
                                                         direction)
283
273
 
284
274
    # rebase merge_depth - unless there are no revisions or 
285
275
    # either the first or last revision have merge_depth = 0.
298
288
        yield revision_id, str(start_revno - num), 0
299
289
 
300
290
 
301
 
def _iter_revisions(repository, view_revisions, generate_delta):
 
291
def make_log_rev_iterator(branch, view_revisions, generate_delta, search):
 
292
    """Create a revision iterator for log.
 
293
 
 
294
    :param branch: The branch being logged.
 
295
    :param view_revisions: The revisions being viewed.
 
296
    :param generate_delta: Whether to generate a delta for each revision.
 
297
    :param search: A user text search string.
 
298
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
299
        delta).
 
300
    """
 
301
    # Convert view_revisions into (view, None, None) groups to fit with
 
302
    # the standard interface here.
 
303
    if type(view_revisions) == list:
 
304
        # A single batch conversion is faster than many incremental ones.
 
305
        # As we have all the data, do a batch conversion.
 
306
        nones = [None] * len(view_revisions)
 
307
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
 
308
    else:
 
309
        def _convert():
 
310
            for view in view_revisions:
 
311
                yield (view, None, None)
 
312
        log_rev_iterator = iter([_convert()])
 
313
    for adapter in log_adapters:
 
314
        log_rev_iterator = adapter(branch, generate_delta, search,
 
315
            log_rev_iterator)
 
316
    return log_rev_iterator
 
317
 
 
318
 
 
319
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
 
320
    """Create a filtered iterator of log_rev_iterator matching on a regex.
 
321
 
 
322
    :param branch: The branch being logged.
 
323
    :param generate_delta: Whether to generate a delta for each revision.
 
324
    :param search: A user text search string.
 
325
    :param log_rev_iterator: An input iterator containing all revisions that
 
326
        could be displayed, in lists.
 
327
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
328
        delta).
 
329
    """
 
330
    if search is None:
 
331
        return log_rev_iterator
 
332
    # Compile the search now to get early errors.
 
333
    searchRE = re.compile(search, re.IGNORECASE)
 
334
    return _filter_message_re(searchRE, log_rev_iterator)
 
335
 
 
336
 
 
337
def _filter_message_re(searchRE, log_rev_iterator):
 
338
    for revs in log_rev_iterator:
 
339
        new_revs = []
 
340
        for (rev_id, revno, merge_depth), rev, delta in revs:
 
341
            if searchRE.search(rev.message):
 
342
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
 
343
        yield new_revs
 
344
 
 
345
 
 
346
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator):
 
347
    """Add revision deltas to a log iterator if needed.
 
348
 
 
349
    :param branch: The branch being logged.
 
350
    :param generate_delta: Whether to generate a delta for each revision.
 
351
    :param search: A user text search string.
 
352
    :param log_rev_iterator: An input iterator containing all revisions that
 
353
        could be displayed, in lists.
 
354
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
355
        delta).
 
356
    """
 
357
    if not generate_delta:
 
358
        return log_rev_iterator
 
359
    return _generate_deltas(branch.repository, log_rev_iterator)
 
360
 
 
361
 
 
362
def _generate_deltas(repository, log_rev_iterator):
 
363
    """Create deltas for each batch of revisions in log_rev_iterator."""
 
364
    for revs in log_rev_iterator:
 
365
        revisions = [rev[1] for rev in revs]
 
366
        deltas = repository.get_deltas_for_revisions(revisions)
 
367
        revs = [(rev[0], rev[1], delta) for rev, delta in izip(revs, deltas)]
 
368
        yield revs
 
369
 
 
370
 
 
371
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
 
372
    """Extract revision objects from the repository
 
373
 
 
374
    :param branch: The branch being logged.
 
375
    :param generate_delta: Whether to generate a delta for each revision.
 
376
    :param search: A user text search string.
 
377
    :param log_rev_iterator: An input iterator containing all revisions that
 
378
        could be displayed, in lists.
 
379
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
380
        delta).
 
381
    """
 
382
    repository = branch.repository
 
383
    for revs in log_rev_iterator:
 
384
        # r = revision_id, n = revno, d = merge depth
 
385
        revision_ids = [view[0] for view, _, _ in revs]
 
386
        revisions = repository.get_revisions(revision_ids)
 
387
        revs = [(rev[0], revision, rev[2]) for rev, revision in
 
388
            izip(revs, revisions)]
 
389
        yield revs
 
390
 
 
391
 
 
392
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
 
393
    """Group up a single large batch into smaller ones.
 
394
 
 
395
    :param branch: The branch being logged.
 
396
    :param generate_delta: Whether to generate a delta for each revision.
 
397
    :param search: A user text search string.
 
398
    :param log_rev_iterator: An input iterator containing all revisions that
 
399
        could be displayed, in lists.
 
400
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev, delta).
 
401
    """
 
402
    repository = branch.repository
302
403
    num = 9
303
 
    view_revisions = iter(view_revisions)
304
 
    while True:
305
 
        cur_view_revisions = [d for x, d in zip(range(num), view_revisions)]
306
 
        if len(cur_view_revisions) == 0:
307
 
            break
308
 
        cur_deltas = {}
309
 
        # r = revision, n = revno, d = merge depth
310
 
        revision_ids = [r for (r, n, d) in cur_view_revisions]
311
 
        revisions = repository.get_revisions(revision_ids)
312
 
        if generate_delta:
313
 
            deltas = repository.get_deltas_for_revisions(revisions)
314
 
            cur_deltas = dict(izip((r.revision_id for r in revisions),
315
 
                                   deltas))
316
 
        for view_data, revision in izip(cur_view_revisions, revisions):
317
 
            yield view_data, revision, cur_deltas.get(revision.revision_id)
318
 
        num = min(int(num * 1.5), 200)
 
404
    for batch in log_rev_iterator:
 
405
        batch = iter(batch)
 
406
        while True:
 
407
            step = [detail for _, detail in zip(range(num), batch)]
 
408
            if len(step) == 0:
 
409
                break
 
410
            yield step
 
411
            num = min(int(num * 1.5), 200)
319
412
 
320
413
 
321
414
def _get_mainline_revs(branch, start_revision, end_revision):
349
442
    if start_revision is None:
350
443
        start_revno = 1
351
444
    else:
352
 
        if isinstance(start_revision, RevisionInfo):
 
445
        if isinstance(start_revision, revisionspec.RevisionInfo):
353
446
            start_rev_id = start_revision.rev_id
354
447
            start_revno = start_revision.revno or 1
355
448
        else:
360
453
    if end_revision is None:
361
454
        end_revno = branch_revno
362
455
    else:
363
 
        if isinstance(end_revision, RevisionInfo):
 
456
        if isinstance(end_revision, revisionspec.RevisionInfo):
364
457
            end_rev_id = end_revision.rev_id
365
458
            end_revno = end_revision.revno or branch_revno
366
459
        else:
367
460
            branch.check_real_revno(end_revision)
368
461
            end_revno = end_revision
369
462
 
370
 
    if ((start_rev_id == NULL_REVISION)
371
 
        or (end_rev_id == NULL_REVISION)):
372
 
        raise BzrCommandError('Logging revision 0 is invalid.')
 
463
    if ((start_rev_id == _mod_revision.NULL_REVISION)
 
464
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
465
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
373
466
    if start_revno > end_revno:
374
 
        raise BzrCommandError("Start revision must be older than "
375
 
                              "the end revision.")
 
467
        raise errors.BzrCommandError("Start revision must be older than "
 
468
                                     "the end revision.")
376
469
 
377
470
    if end_revno < start_revno:
378
471
        return None, None, None, None
443
536
    return view_revisions
444
537
 
445
538
 
446
 
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
447
 
                                       view_revs_iter):
448
 
    """Return the list of revision ids which touch a given file id.
 
539
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
540
                                       direction):
 
541
    r"""Return the list of revision ids which touch a given file id.
449
542
 
450
543
    The function filters view_revisions and returns a subset.
451
544
    This includes the revisions which directly change the file id,
452
545
    and the revisions which merge these changes. So if the
453
546
    revision graph is::
454
 
        A
455
 
        |\
456
 
        B C
 
547
        A-.
 
548
        |\ \
 
549
        B C E
 
550
        |/ /
 
551
        D |
 
552
        |\|
 
553
        | F
457
554
        |/
458
 
        D
459
 
 
460
 
    And 'C' changes a file, then both C and D will be returned.
461
 
 
462
 
    This will also can be restricted based on a subset of the mainline.
463
 
 
 
555
        G
 
556
 
 
557
    And 'C' changes a file, then both C and D will be returned. F will not be
 
558
    returned even though it brings the changes to C into the branch starting
 
559
    with E. (Note that if we were using F as the tip instead of G, then we
 
560
    would see C, D, F.)
 
561
 
 
562
    This will also be restricted based on a subset of the mainline.
 
563
 
 
564
    :param branch: The branch where we can get text revision information.
 
565
    :param file_id: Filter out revisions that do not touch file_id.
 
566
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
567
        tuples. This is the list of revisions which will be filtered. It is
 
568
        assumed that view_revisions is in merge_sort order (either forward or
 
569
        reverse).
 
570
    :param direction: The direction of view_revisions.  See also
 
571
        reverse_by_depth, and get_view_revisions
464
572
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
465
573
    """
466
 
    # find all the revisions that change the specific file
467
 
    file_weave = branch.repository.weave_store.get_weave(file_id,
468
 
                branch.repository.get_transaction())
469
 
    weave_modifed_revisions = set(file_weave.versions())
470
 
    # build the ancestry of each revision in the graph
471
 
    # - only listing the ancestors that change the specific file.
472
 
    graph = branch.repository.get_graph()
473
 
    # This asks for all mainline revisions, which means we only have to spider
474
 
    # sideways, rather than depth history. That said, its still size-of-history
475
 
    # and should be addressed.
476
 
    # mainline_revisions always includes an extra revision at the beginning, so
477
 
    # don't request it.
478
 
    parent_map = dict(((key, value) for key, value in
479
 
        graph.iter_ancestry(mainline_revisions[1:]) if value is not None))
480
 
    sorted_rev_list = topo_sort(parent_map.items())
481
 
    ancestry = {}
482
 
    for rev in sorted_rev_list:
483
 
        parents = parent_map[rev]
484
 
        if rev not in weave_modifed_revisions and len(parents) == 1:
485
 
            # We will not be adding anything new, so just use a reference to
486
 
            # the parent ancestry.
487
 
            rev_ancestry = ancestry[parents[0]]
 
574
    # Lookup all possible text keys to determine which ones actually modified
 
575
    # the file.
 
576
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
 
577
    # Looking up keys in batches of 1000 can cut the time in half, as well as
 
578
    # memory consumption. GraphIndex *does* like to look for a few keys in
 
579
    # parallel, it just doesn't like looking for *lots* of keys in parallel.
 
580
    # TODO: This code needs to be re-evaluated periodically as we tune the
 
581
    #       indexing layer. We might consider passing in hints as to the known
 
582
    #       access pattern (sparse/clustered, high success rate/low success
 
583
    #       rate). This particular access is clustered with a low success rate.
 
584
    get_parent_map = branch.repository.texts.get_parent_map
 
585
    modified_text_revisions = set()
 
586
    chunk_size = 1000
 
587
    for start in xrange(0, len(text_keys), chunk_size):
 
588
        next_keys = text_keys[start:start + chunk_size]
 
589
        # Only keep the revision_id portion of the key
 
590
        modified_text_revisions.update(
 
591
            [k[1] for k in get_parent_map(next_keys)])
 
592
    del text_keys, next_keys
 
593
 
 
594
    result = []
 
595
    if direction == 'forward':
 
596
        # TODO: The algorithm for finding 'merges' of file changes expects
 
597
        #       'reverse' order (the default from 'merge_sort()'). Instead of
 
598
        #       forcing this, we could just use the reverse_by_depth order.
 
599
        view_revisions = reverse_by_depth(view_revisions)
 
600
    # Track what revisions will merge the current revision, replace entries
 
601
    # with 'None' when they have been added to result
 
602
    current_merge_stack = [None]
 
603
    for info in view_revisions:
 
604
        rev_id, revno, depth = info
 
605
        if depth == len(current_merge_stack):
 
606
            current_merge_stack.append(info)
488
607
        else:
489
 
            rev_ancestry = set()
490
 
            if rev in weave_modifed_revisions:
491
 
                rev_ancestry.add(rev)
492
 
            for parent in parents:
493
 
                if parent not in ancestry:
494
 
                    # parent is a Ghost, which won't be present in
495
 
                    # sorted_rev_list, but we may access it later, so create an
496
 
                    # empty node for it
497
 
                    ancestry[parent] = set()
498
 
                rev_ancestry = rev_ancestry.union(ancestry[parent])
499
 
        ancestry[rev] = rev_ancestry
500
 
 
501
 
    def is_merging_rev(r):
502
 
        parents = parent_map[r]
503
 
        if len(parents) > 1:
504
 
            leftparent = parents[0]
505
 
            for rightparent in parents[1:]:
506
 
                if not ancestry[leftparent].issuperset(
507
 
                        ancestry[rightparent]):
508
 
                    return True
509
 
        return False
510
 
 
511
 
    # filter from the view the revisions that did not change or merge 
512
 
    # the specific file
513
 
    return [(r, n, d) for r, n, d in view_revs_iter
514
 
            if r in weave_modifed_revisions or is_merging_rev(r)]
 
608
            del current_merge_stack[depth + 1:]
 
609
            current_merge_stack[-1] = info
 
610
 
 
611
        if rev_id in modified_text_revisions:
 
612
            # This needs to be logged, along with the extra revisions
 
613
            for idx in xrange(len(current_merge_stack)):
 
614
                node = current_merge_stack[idx]
 
615
                if node is not None:
 
616
                    result.append(node)
 
617
                    current_merge_stack[idx] = None
 
618
    if direction == 'forward':
 
619
        result = reverse_by_depth(result)
 
620
    return result
515
621
 
516
622
 
517
623
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
536
642
    parent_map = dict(((key, value) for key, value in
537
643
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
538
644
    # filter out ghosts; merge_sort errors on ghosts.
539
 
    rev_graph = _strip_NULL_ghosts(parent_map)
540
 
    merge_sorted_revisions = merge_sort(
 
645
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
 
646
    merge_sorted_revisions = tsort.merge_sort(
541
647
        rev_graph,
542
648
        mainline_revs[-1],
543
649
        mainline_revs,
615
721
        only relevant if supports_merge_revisions is not True.
616
722
    - supports_tags must be True if this log formatter supports tags.
617
723
        Otherwise the tags attribute may not be populated.
 
724
 
 
725
    Plugins can register functions to show custom revision properties using
 
726
    the properties_handler_registry. The registered function
 
727
    must respect the following interface description:
 
728
        def my_show_properties(properties_dict):
 
729
            # code that returns a dict {'name':'value'} of the properties 
 
730
            # to be shown
618
731
    """
619
732
 
620
733
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
644
757
            return name
645
758
        return address
646
759
 
 
760
    def show_properties(self, revision, indent):
 
761
        """Displays the custom properties returned by each registered handler.
 
762
        
 
763
        If a registered handler raises an error it is propagated.
 
764
        """
 
765
        for key, handler in properties_handler_registry.iteritems():
 
766
            for key, value in handler(revision).items():
 
767
                self.to_file.write(indent + key + ': ' + value + '\n')
 
768
 
647
769
 
648
770
class LongLogFormatter(LogFormatter):
649
771
 
665
787
            to_file.write('\n')
666
788
            for parent_id in revision.rev.parent_ids:
667
789
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
 
790
        self.show_properties(revision.rev, indent)
668
791
 
669
792
        author = revision.rev.properties.get('author', None)
670
793
        if author is not None:
698
821
 
699
822
    def log_revision(self, revision):
700
823
        to_file = self.to_file
701
 
        date_str = format_date(revision.rev.timestamp,
702
 
                               revision.rev.timezone or 0,
703
 
                               self.show_timezone)
704
824
        is_merge = ''
705
825
        if len(revision.rev.parent_ids) > 1:
706
826
            is_merge = ' [merge]'
758
878
 
759
879
    def log_string(self, revno, rev, max_chars):
760
880
        """Format log info into one string. Truncate tail of string
761
 
        :param  revno:      revision number (int) or None.
 
881
        :param  revno:      revision number or None.
762
882
                            Revision numbers counts from 1.
763
883
        :param  rev:        revision info object
764
884
        :param  max_chars:  maximum length of resulting string
818
938
    try:
819
939
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
820
940
    except KeyError:
821
 
        raise BzrCommandError("unknown log formatter: %r" % name)
 
941
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
822
942
 
823
943
 
824
944
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
881
1001
                 end_revision=len(new_rh),
882
1002
                 search=None)
883
1003
 
 
1004
 
 
1005
properties_handler_registry = registry.Registry()
 
1006
 
 
1007
# adapters which revision ids to log are filtered. When log is called, the
 
1008
# log_rev_iterator is adapted through each of these factory methods.
 
1009
# Plugins are welcome to mutate this list in any way they like - as long
 
1010
# as the overall behaviour is preserved. At this point there is no extensible
 
1011
# mechanism for getting parameters to each factory method, and until there is
 
1012
# this won't be considered a stable api.
 
1013
log_adapters = [
 
1014
    # core log logic
 
1015
    _make_batch_filter,
 
1016
    # read revision objects
 
1017
    _make_revision_objects,
 
1018
    # filter on log messages
 
1019
    _make_search_filter,
 
1020
    # generate deltas for things we will show
 
1021
    _make_delta_filter
 
1022
    ]