~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2007-10-29 16:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 141.
  • Revision ID: michael.hudson@canonical.com-20071029161930-oxqrd4rd8j1oz3hx
add do nothing check target

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
 
28
28
 
29
29
import bisect
 
30
import cgi
30
31
import datetime
31
32
import logging
 
33
import os
 
34
import posixpath
32
35
import re
 
36
import shelve
 
37
import sys
33
38
import textwrap
34
39
import threading
35
40
import time
39
44
from loggerhead.util import decorator
40
45
 
41
46
import bzrlib
 
47
import bzrlib.annotate
42
48
import bzrlib.branch
43
49
import bzrlib.bundle.serializer
 
50
import bzrlib.decorators
44
51
import bzrlib.diff
45
52
import bzrlib.errors
46
53
import bzrlib.progress
47
54
import bzrlib.revision
 
55
import bzrlib.textfile
48
56
import bzrlib.tsort
49
57
import bzrlib.ui
50
58
 
52
60
with_branch_lock = util.with_lock('_lock', 'branch')
53
61
 
54
62
 
55
 
@decorator
56
 
def with_bzrlib_read_lock(unbound):
57
 
    def bzrlib_read_locked(self, *args, **kw):
58
 
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
59
 
        self._branch.repository.lock_read()
60
 
        try:
61
 
            return unbound(self, *args, **kw)
62
 
        finally:
63
 
            self._branch.repository.unlock()
64
 
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
65
 
    return bzrlib_read_locked
66
 
 
67
 
 
68
63
# bzrlib's UIFactory is not thread-safe
69
64
uihack = threading.local()
70
65
 
155
150
 
156
151
    # Make short form of commit message.
157
152
    short_message = message[0]
158
 
    if len(short_message) > 60:
159
 
        short_message = short_message[:60] + '...'
 
153
    if len(short_message) > 80:
 
154
        short_message = short_message[:80] + '...'
160
155
 
161
156
    return message, short_message
162
157
 
191
186
class History (object):
192
187
 
193
188
    def __init__(self):
 
189
        self._change_cache = None
194
190
        self._file_change_cache = None
 
191
        self._index = None
195
192
        self._lock = threading.RLock()
196
193
 
197
194
    @classmethod
198
 
    def from_branch(cls, branch):
 
195
    def from_branch(cls, branch, name=None):
199
196
        z = time.time()
200
197
        self = cls()
201
198
        self._branch = branch
202
199
        self._last_revid = self._branch.last_revision()
203
 
 
204
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch.nick,))
205
 
 
206
 
        graph = branch.repository.get_graph()
207
 
        parent_map = dict(((key, value) for key, value in
208
 
             graph.iter_ancestry([self._last_revid]) if value is not None))
209
 
 
210
 
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
 
200
        if self._last_revid is not None:
 
201
            self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
 
202
        else:
 
203
            self._revision_graph = {}
 
204
 
 
205
        if name is None:
 
206
            name = self._branch.nick
 
207
        self._name = name
 
208
        self.log = logging.getLogger('loggerhead.%s' % (name,))
 
209
 
211
210
        self._full_history = []
212
211
        self._revision_info = {}
213
212
        self._revno_revid = {}
214
 
        if bzrlib.revision.is_null(self._last_revid):
215
 
            self._merge_sort = []
216
 
        else:
217
 
            self._merge_sort = bzrlib.tsort.merge_sort(
218
 
                self._revision_graph, self._last_revid, generate_revno=True)
219
 
 
 
213
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
220
214
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
221
215
            self._full_history.append(revid)
222
216
            revno_str = '.'.join(str(n) for n in revno)
223
217
            self._revno_revid[revno_str] = revid
224
 
            self._revision_info[revid] = (
225
 
                seq, revid, merge_depth, revno_str, end_of_merge)
 
218
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
226
219
 
227
220
        # cache merge info
228
221
        self._where_merged = {}
229
 
 
230
222
        for revid in self._revision_graph.keys():
231
 
            if self._revision_info[revid][2] == 0:
 
223
            if not revid in self._full_history:
232
224
                continue
233
225
            for parent in self._revision_graph[revid]:
234
226
                self._where_merged.setdefault(parent, set()).add(revid)
236
228
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
237
229
        return self
238
230
 
239
 
    @staticmethod
240
 
    def _strip_NULL_ghosts(revision_graph):
241
 
        """
242
 
        Copied over from bzrlib meant as a temporary workaround deprecated 
243
 
        methods.
244
 
        """
245
 
 
246
 
        # Filter ghosts, and null:
247
 
        if bzrlib.revision.NULL_REVISION in revision_graph:
248
 
            del revision_graph[bzrlib.revision.NULL_REVISION]
249
 
        for key, parents in revision_graph.items():
250
 
            revision_graph[key] = tuple(parent for parent in parents if parent
251
 
                in revision_graph)
252
 
        return revision_graph
253
 
 
254
231
    @classmethod
255
 
    def from_folder(cls, path):
 
232
    def from_folder(cls, path, name=None):
256
233
        b = bzrlib.branch.Branch.open(path)
257
 
        b.lock_read()
258
 
        try:
259
 
            return cls.from_branch(b)
260
 
        finally:
261
 
            b.unlock()
 
234
        return cls.from_branch(b, name)
262
235
 
263
236
    @with_branch_lock
264
237
    def out_of_date(self):
265
238
        # the branch may have been upgraded on disk, in which case we're stale.
266
 
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
267
239
        if self._branch.__class__ is not \
268
 
               newly_opened.__class__:
269
 
            return True
270
 
        if self._branch.repository.__class__ is not \
271
 
               newly_opened.repository.__class__:
 
240
               bzrlib.branch.Branch.open(self._branch.base).__class__:
272
241
            return True
273
242
        return self._branch.last_revision() != self._last_revid
274
243
 
 
244
    def use_cache(self, cache):
 
245
        self._change_cache = cache
 
246
 
275
247
    def use_file_cache(self, cache):
276
248
        self._file_change_cache = cache
277
249
 
278
 
    @property
279
 
    def has_revisions(self):
280
 
        return not bzrlib.revision.is_null(self.last_revid)
 
250
    def use_search_index(self, index):
 
251
        self._index = index
 
252
 
 
253
    @with_branch_lock
 
254
    def detach(self):
 
255
        # called when a new history object needs to be created, because the
 
256
        # branch history has changed.  we need to immediately close and stop
 
257
        # using our caches, because a new history object will be created to
 
258
        # replace us, using the same cache files.
 
259
        # (may also be called during server shutdown.)
 
260
        if self._change_cache is not None:
 
261
            self._change_cache.close()
 
262
            self._change_cache = None
 
263
        if self._index is not None:
 
264
            self._index.close()
 
265
            self._index = None
 
266
 
 
267
    def flush_cache(self):
 
268
        if self._change_cache is None:
 
269
            return
 
270
        self._change_cache.flush()
 
271
 
 
272
    def check_rebuild(self):
 
273
        if self._change_cache is not None:
 
274
            self._change_cache.check_rebuild()
 
275
        if self._index is not None:
 
276
            self._index.check_rebuild()
281
277
 
282
278
    last_revid = property(lambda self: self._last_revid, None, None)
283
279
 
295
291
    def get_revision_history(self):
296
292
        return self._full_history
297
293
 
298
 
    def get_revids_from(self, revid_list, start_revid):
299
 
        """
300
 
        Yield the mainline (wrt start_revid) revisions that merged each
301
 
        revid in revid_list.
302
 
        """
303
 
        if revid_list is None:
304
 
            revid_list = self._full_history
305
 
        revid_set = set(revid_list)
306
 
        revid = start_revid
307
 
        def introduced_revisions(revid):
308
 
            r = set([revid])
309
 
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
310
 
            i = seq + 1
311
 
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
312
 
                r.add(self._merge_sort[i][1])
313
 
                i += 1
314
 
            return r
315
 
        while 1:
316
 
            if bzrlib.revision.is_null(revid):
317
 
                return
318
 
            if introduced_revisions(revid) & revid_set:
 
294
    def get_revids_from(self, revid_list, revid):
 
295
        """
 
296
        given a list of revision ids, yield revisions in graph order,
 
297
        starting from revid.  the list can be None if you just want to travel
 
298
        across all revisions.
 
299
        """
 
300
        while True:
 
301
            if (revid_list is None) or (revid in revid_list):
319
302
                yield revid
 
303
            if not self._revision_graph.has_key(revid):
 
304
                return
320
305
            parents = self._revision_graph[revid]
321
306
            if len(parents) == 0:
322
307
                return
348
333
        return revid_list[index:]
349
334
 
350
335
    @with_branch_lock
 
336
    def get_revision_history_matching(self, revid_list, text):
 
337
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
338
        z = time.time()
 
339
        # this is going to be painfully slow. :(
 
340
        out = []
 
341
        text = text.lower()
 
342
        for revid in revid_list:
 
343
            change = self.get_changes([ revid ])[0]
 
344
            if text in change.comment.lower():
 
345
                out.append(revid)
 
346
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
 
347
        return out
 
348
 
 
349
    def get_revision_history_matching_indexed(self, revid_list, text):
 
350
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
351
        z = time.time()
 
352
        if self._index is None:
 
353
            return self.get_revision_history_matching(revid_list, text)
 
354
        out = self._index.find(text, revid_list)
 
355
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
 
356
        # put them in some coherent order :)
 
357
        out = [r for r in self._full_history if r in out]
 
358
        return out
 
359
 
 
360
    @with_branch_lock
351
361
    def get_search_revid_list(self, query, revid_list):
352
362
        """
353
363
        given a "quick-search" query, try a few obvious possible meanings:
389
399
                revid_list = list(self.get_revids_from(None, self._last_revid))
390
400
            return self.get_revision_history_since(revid_list, date)
391
401
 
 
402
        # check comment fields.
 
403
        if revid_list is None:
 
404
            revid_list = self._full_history
 
405
        return self.get_revision_history_matching_indexed(revid_list, query)
 
406
 
392
407
    revno_re = re.compile(r'^[\d\.]+$')
393
408
    # the date regex are without a final '$' so that queries like
394
409
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
434
449
        determine the revision list we're viewing (start_revid, file_id, query)
435
450
        and where we are in it (revid).
436
451
 
437
 
            - if a query is given, we're viewing query results.
438
 
            - if a file_id is given, we're viewing revisions for a specific
439
 
              file.
440
 
            - if a start_revid is given, we're viewing the branch from a
441
 
              specific revision up the tree.
442
 
 
443
 
        these may be combined to view revisions for a specific file, from
444
 
        a specific revision, with a specific search query.
445
 
 
446
 
        returns a new (revid, start_revid, revid_list) where:
 
452
        if a query is given, we're viewing query results.
 
453
        if a file_id is given, we're viewing revisions for a specific file.
 
454
        if a start_revid is given, we're viewing the branch from a
 
455
            specific revision up the tree.
 
456
        (these may be combined to view revisions for a specific file, from
 
457
            a specific revision, with a specific search query.)
 
458
 
 
459
        returns a new (revid, start_revid, revid_list, scan_list) where:
447
460
 
448
461
            - revid: current position within the view
449
462
            - start_revid: starting revision of this view
462
475
            if revid not in revid_list:
463
476
                # if the given revid is not in the revlist, use a revlist that
464
477
                # starts at the given revid.
465
 
                revid_list = self.get_file_view(revid, file_id)
 
478
                revid_list= self.get_file_view(revid, file_id)
466
479
                start_revid = revid
467
480
            return revid, start_revid, revid_list
468
481
 
473
486
            revid_list = None
474
487
 
475
488
        revid_list = self.get_search_revid_list(query, revid_list)
476
 
        if revid_list and len(revid_list) > 0:
 
489
        if len(revid_list) > 0:
477
490
            if revid not in revid_list:
478
491
                revid = revid_list[0]
479
492
            return revid, start_revid, revid_list
480
493
        else:
 
494
            # no results
481
495
            return None, None, []
482
496
 
483
497
    @with_branch_lock
499
513
            path = '/' + path
500
514
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
501
515
 
 
516
 
502
517
    def get_merge_point_list(self, revid):
503
518
        """
504
519
        Return the list of revids that have merged this node.
574
589
 
575
590
    @with_branch_lock
576
591
    def get_changes(self, revid_list):
577
 
        """Return a list of changes objects for the given revids.
578
 
 
579
 
        Revisions not present and NULL_REVISION will be ignored.
580
 
        """
581
 
        changes = self.get_changes_uncached(revid_list)
 
592
        if self._change_cache is None:
 
593
            changes = self.get_changes_uncached(revid_list)
 
594
        else:
 
595
            changes = self._change_cache.get_changes(revid_list)
582
596
        if len(changes) == 0:
583
597
            return changes
584
598
 
587
601
        for change in changes:
588
602
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
589
603
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
590
 
            if len(change.parents) > 0:
591
 
                change.parents = [util.Container(revid=r, 
592
 
                    revno=self.get_revno(r)) for r in change.parents]
593
604
            change.revno = self.get_revno(change.revid)
594
605
 
595
606
        parity = 0
599
610
 
600
611
        return changes
601
612
 
602
 
    @with_branch_lock
603
 
    @with_bzrlib_read_lock
604
 
    def get_changes_uncached(self, revid_list):
605
 
        # FIXME: deprecated method in getting a null revision
606
 
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
607
 
                            revid_list)
608
 
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
609
 
        # We need to return the answer in the same order as the input,
610
 
        # less any ghosts.
611
 
        present_revids = [revid for revid in revid_list
612
 
                          if revid in parent_map]
613
 
        rev_list = self._branch.repository.get_revisions(present_revids)
614
 
 
615
 
        return [self._change_from_revision(rev) for rev in rev_list]
616
 
 
617
 
    def _get_deltas_for_revisions_with_trees(self, revisions):
618
 
        """Produce a list of revision deltas.
 
613
    # alright, let's profile this sucka.
 
614
    def _get_changes_profiled(self, revid_list, get_diffs=False):
 
615
        from loggerhead.lsprof import profile
 
616
        import cPickle
 
617
        ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
 
618
        stats.sort()
 
619
        stats.freeze()
 
620
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
 
621
        self.log.info('lsprof complete!')
 
622
        return ret
 
623
 
 
624
    def _get_deltas_for_revisions_with_trees(self, entries):
 
625
        """Produce a generator of revision deltas.
619
626
 
620
627
        Note that the input is a sequence of REVISIONS, not revision_ids.
621
628
        Trees will be held in memory until the generator exits.
622
629
        Each delta is relative to the revision's lefthand predecessor.
623
 
        (This is copied from bzrlib.)
624
630
        """
625
631
        required_trees = set()
626
 
        for revision in revisions:
627
 
            required_trees.add(revision.revid)
628
 
            required_trees.update([p.revid for p in revision.parents[:1]])
 
632
        for entry in entries:
 
633
            required_trees.add(entry.revid)
 
634
            required_trees.update([p.revid for p in entry.parents[:1]])
629
635
        trees = dict((t.get_revision_id(), t) for
630
636
                     t in self._branch.repository.revision_trees(required_trees))
631
637
        ret = []
632
638
        self._branch.repository.lock_read()
633
639
        try:
634
 
            for revision in revisions:
635
 
                if not revision.parents:
 
640
            for entry in entries:
 
641
                if not entry.parents:
636
642
                    old_tree = self._branch.repository.revision_tree(
637
643
                        bzrlib.revision.NULL_REVISION)
638
644
                else:
639
 
                    old_tree = trees[revision.parents[0].revid]
640
 
                tree = trees[revision.revid]
 
645
                    old_tree = trees[entry.parents[0].revid]
 
646
                tree = trees[entry.revid]
641
647
                ret.append(tree.changes_from(old_tree))
642
648
            return ret
643
649
        finally:
644
650
            self._branch.repository.unlock()
645
651
 
646
 
    def _change_from_revision(self, revision):
647
 
        """
648
 
        Given a bzrlib Revision, return a processed "change" for use in
649
 
        templates.
650
 
        """
 
652
    def entry_from_revision(self, revision):
651
653
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
652
654
 
653
655
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
662
664
            'short_comment': short_message,
663
665
            'comment': revision.message,
664
666
            'comment_clean': [util.html_clean(s) for s in message],
665
 
            'parents': revision.parent_ids,
 
667
            'parents': parents,
666
668
        }
667
669
        return util.Container(entry)
668
670
 
 
671
    @with_branch_lock
 
672
    def get_changes_uncached(self, revid_list):
 
673
        # Because we may loop and call get_revisions multiple times (to throw
 
674
        # out dud revids), we grab a read lock.
 
675
        self._branch.lock_read()
 
676
        try:
 
677
            while True:
 
678
                try:
 
679
                    rev_list = self._branch.repository.get_revisions(revid_list)
 
680
                except (KeyError, bzrlib.errors.NoSuchRevision), e:
 
681
                    # this sometimes happens with arch-converted branches.
 
682
                    # i don't know why. :(
 
683
                    self.log.debug('No such revision (skipping): %s', e)
 
684
                    revid_list.remove(e.revision)
 
685
                else:
 
686
                    break
 
687
 
 
688
            return [self.entry_from_revision(rev) for rev in rev_list]
 
689
        finally:
 
690
            self._branch.unlock()
 
691
 
669
692
    def get_file_changes_uncached(self, entries):
670
693
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
671
694
 
686
709
 
687
710
    @with_branch_lock
688
711
    def get_change_with_diff(self, revid, compare_revid=None):
689
 
        change = self.get_changes([revid])[0]
 
712
        entry = self.get_changes([revid])[0]
690
713
 
691
714
        if compare_revid is None:
692
 
            if change.parents:
693
 
                compare_revid = change.parents[0].revid
 
715
            if entry.parents:
 
716
                compare_revid = entry.parents[0].revid
694
717
            else:
695
718
                compare_revid = 'null:'
696
719
 
698
721
        rev_tree2 = self._branch.repository.revision_tree(revid)
699
722
        delta = rev_tree2.changes_from(rev_tree1)
700
723
 
701
 
        change.changes = self.parse_delta(delta)
702
 
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
703
 
 
704
 
        return change
 
724
        entry.changes = self.parse_delta(delta)
 
725
 
 
726
        entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
727
 
 
728
        return entry
705
729
 
706
730
    @with_branch_lock
707
731
    def get_file(self, file_id, revid):
754
778
                    diff = buffer.getvalue()
755
779
            else:
756
780
                diff = ''
757
 
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
 
781
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
758
782
 
759
783
        return out
760
784
 
894
918
 
895
919
        file_revid = self.get_inventory(revid)[file_id].revision
896
920
        oldvalues = None
897
 
        tree = self._branch.repository.revision_tree(file_revid)
 
921
 
 
922
        # because we cache revision metadata ourselves, it's actually much
 
923
        # faster to call 'annotate_iter' on the weave directly than it is to
 
924
        # ask bzrlib to annotate for us.
 
925
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
926
 
898
927
        revid_set = set()
899
 
 
900
 
        for line_revid, text in tree.annotate_iter(file_id):
 
928
        for line_revid, text in w.annotate_iter(file_revid):
901
929
            revid_set.add(line_revid)
902
930
            if self._BADCHARS_RE.match(text):
903
931
                # bail out; this isn't displayable text
905
933
                                     text='(This is a binary file.)',
906
934
                                     change=util.Container())
907
935
                return
908
 
        change_cache = dict([(c.revid, c) \
909
 
                for c in self.get_changes(list(revid_set))])
 
936
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
910
937
 
911
938
        last_line_revid = None
912
 
        for line_revid, text in tree.annotate_iter(file_id):
 
939
        for line_revid, text in w.annotate_iter(file_revid):
913
940
            if line_revid == last_line_revid:
914
941
                # remember which lines have a new revno and which don't
915
942
                status = 'same'
939
966
        s = StringIO()
940
967
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
941
968
        return s.getvalue()
 
969