~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Martin Albisetti
  • Date: 2008-07-25 02:01:50 UTC
  • mto: (157.1.3 loggerhead)
  • mto: This revision was merged to the branch mainline in revision 187.
  • Revision ID: argentina@gmail.com-20080725020150-f3oyamfyrv8cc79b
Let the server order again

Show diffs side-by-side

added added

removed removed

Lines of Context:
35
35
import time
36
36
from StringIO import StringIO
37
37
 
 
38
from loggerhead import search
38
39
from loggerhead import util
39
 
from loggerhead.util import decorator
 
40
from loggerhead.wholehistory import compute_whole_history_data
40
41
 
41
42
import bzrlib
42
43
import bzrlib.branch
43
 
import bzrlib.bundle.serializer
44
44
import bzrlib.diff
45
45
import bzrlib.errors
46
46
import bzrlib.progress
48
48
import bzrlib.tsort
49
49
import bzrlib.ui
50
50
 
51
 
 
52
 
with_branch_lock = util.with_lock('_lock', 'branch')
53
 
 
54
 
 
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
51
# bzrlib's UIFactory is not thread-safe
69
52
uihack = threading.local()
70
53
 
108
91
    out_chunk_list = []
109
92
    for chunk in chunk_list:
110
93
        line_list = []
 
94
        wrap_char = '<wbr/>'
111
95
        delete_list, insert_list = [], []
112
96
        for line in chunk.diff:
 
97
            # Add <wbr/> every X characters so we can wrap properly
 
98
            wrap_line = re.findall(r'.{%d}|.+$' % 78, line.line)
 
99
            wrap_lines = [util.html_clean(_line) for _line in wrap_line]
 
100
            wrapped_line = wrap_char.join(wrap_lines)
 
101
 
113
102
            if line.type == 'context':
114
103
                if len(delete_list) or len(insert_list):
115
 
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
104
                    _process_side_by_side_buffers(line_list, delete_list, 
 
105
                                                  insert_list)
116
106
                    delete_list, insert_list = [], []
117
 
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
118
 
                                                old_line=line.line, new_line=line.line,
119
 
                                                old_type=line.type, new_type=line.type))
 
107
                line_list.append(util.Container(old_lineno=line.old_lineno, 
 
108
                                                new_lineno=line.new_lineno,
 
109
                                                old_line=wrapped_line, 
 
110
                                                new_line=wrapped_line,
 
111
                                                old_type=line.type, 
 
112
                                                new_type=line.type))
120
113
            elif line.type == 'delete':
121
 
                delete_list.append((line.old_lineno, line.line, line.type))
 
114
                delete_list.append((line.old_lineno, wrapped_line, line.type))
122
115
            elif line.type == 'insert':
123
 
                insert_list.append((line.new_lineno, line.line, line.type))
 
116
                insert_list.append((line.new_lineno, wrapped_line, line.type))
124
117
        if len(delete_list) or len(insert_list):
125
118
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
126
119
        out_chunk_list.append(util.Container(diff=line_list))
189
182
 
190
183
 
191
184
class History (object):
192
 
 
193
 
    def __init__(self):
194
 
        self._change_cache = None
 
185
    """Decorate a branch to provide information for rendering.
 
186
 
 
187
    History objects are expected to be short lived -- when serving a request
 
188
    for a particular branch, open it, read-lock it, wrap a History object
 
189
    around it, serve the request, throw the History object away, unlock the
 
190
    branch and throw it away.
 
191
 
 
192
    :ivar _file_change_cache: xx
 
193
    """
 
194
 
 
195
    def __init__(self, branch, whole_history_data_cache):
 
196
        assert branch.is_locked(), (
 
197
            "Can only construct a History object with a read-locked branch.")
195
198
        self._file_change_cache = None
196
 
        self._index = None
197
 
        self._lock = threading.RLock()
198
 
 
199
 
    @classmethod
200
 
    def from_branch(cls, branch, name=None):
201
 
        z = time.time()
202
 
        self = cls()
203
199
        self._branch = branch
204
 
        self._last_revid = self._branch.last_revision()
205
 
 
206
 
        if name is None:
207
 
            name = self._branch.nick
208
 
        self._name = name
209
 
        self.log = logging.getLogger('loggerhead.%s' % (name,))
210
 
 
211
 
        graph = branch.repository.get_graph()
212
 
        parent_map = dict(((key, value) for key, value in
213
 
             graph.iter_ancestry([self._last_revid]) if value is not None))
214
 
 
215
 
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
216
 
        self._full_history = []
217
 
        self._revision_info = {}
218
 
        self._revno_revid = {}
219
 
        if bzrlib.revision.is_null(self._last_revid):
220
 
            self._merge_sort = []
221
 
        else:
222
 
            self._merge_sort = bzrlib.tsort.merge_sort(
223
 
                self._revision_graph, self._last_revid, generate_revno=True)
224
 
 
225
 
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
226
 
            self._full_history.append(revid)
227
 
            revno_str = '.'.join(str(n) for n in revno)
228
 
            self._revno_revid[revno_str] = revid
229
 
            self._revision_info[revid] = (
230
 
                seq, revid, merge_depth, revno_str, end_of_merge)
231
 
 
232
 
        # cache merge info
233
 
        self._where_merged = {}
234
 
 
235
 
        for revid in self._revision_graph.keys():
236
 
            if self._revision_info[revid][2] == 0:
237
 
                continue
238
 
            for parent in self._revision_graph[revid]:
239
 
                self._where_merged.setdefault(parent, set()).add(revid)
240
 
 
241
 
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
242
 
        return self
243
 
 
244
 
    @staticmethod
245
 
    def _strip_NULL_ghosts(revision_graph):
246
 
        """
247
 
        Copied over from bzrlib meant as a temporary workaround deprecated 
248
 
        methods.
249
 
        """
250
 
 
251
 
        # Filter ghosts, and null:
252
 
        if bzrlib.revision.NULL_REVISION in revision_graph:
253
 
            del revision_graph[bzrlib.revision.NULL_REVISION]
254
 
        for key, parents in revision_graph.items():
255
 
            revision_graph[key] = tuple(parent for parent in parents if parent
256
 
                in revision_graph)
257
 
        return revision_graph
258
 
 
259
 
    @classmethod
260
 
    def from_folder(cls, path, name=None):
261
 
        b = bzrlib.branch.Branch.open(path)
262
 
        b.lock_read()
263
 
        try:
264
 
            return cls.from_branch(b, name)
265
 
        finally:
266
 
            b.unlock()
267
 
 
268
 
    @with_branch_lock
269
 
    def out_of_date(self):
270
 
        # the branch may have been upgraded on disk, in which case we're stale.
271
 
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
272
 
        if self._branch.__class__ is not \
273
 
               newly_opened.__class__:
274
 
            return True
275
 
        if self._branch.repository.__class__ is not \
276
 
               newly_opened.repository.__class__:
277
 
            return True
278
 
        return self._branch.last_revision() != self._last_revid
279
 
 
280
 
    def use_cache(self, cache):
281
 
        self._change_cache = cache
 
200
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
 
201
 
 
202
        self.last_revid = branch.last_revision()
 
203
 
 
204
        whole_history_data = whole_history_data_cache.get(self.last_revid)
 
205
        if whole_history_data is None:
 
206
            whole_history_data = compute_whole_history_data(branch)
 
207
            whole_history_data_cache[self.last_revid] = whole_history_data
 
208
 
 
209
        (self._revision_graph, self._full_history, self._revision_info,
 
210
         self._revno_revid, self._merge_sort, self._where_merged
 
211
         ) = whole_history_data
282
212
 
283
213
    def use_file_cache(self, cache):
284
214
        self._file_change_cache = cache
285
215
 
286
 
    def use_search_index(self, index):
287
 
        self._index = index
288
 
 
289
216
    @property
290
217
    def has_revisions(self):
291
218
        return not bzrlib.revision.is_null(self.last_revid)
292
219
 
293
 
    @with_branch_lock
294
 
    def detach(self):
295
 
        # called when a new history object needs to be created, because the
296
 
        # branch history has changed.  we need to immediately close and stop
297
 
        # using our caches, because a new history object will be created to
298
 
        # replace us, using the same cache files.
299
 
        # (may also be called during server shutdown.)
300
 
        if self._change_cache is not None:
301
 
            self._change_cache.close()
302
 
            self._change_cache = None
303
 
        if self._index is not None:
304
 
            self._index.close()
305
 
            self._index = None
306
 
 
307
 
    def flush_cache(self):
308
 
        if self._change_cache is None:
309
 
            return
310
 
        self._change_cache.flush()
311
 
 
312
 
    def check_rebuild(self):
313
 
        if self._change_cache is not None:
314
 
            self._change_cache.check_rebuild()
315
 
        #if self._index is not None:
316
 
        #    self._index.check_rebuild()
317
 
 
318
 
    last_revid = property(lambda self: self._last_revid, None, None)
319
 
 
320
 
    @with_branch_lock
321
220
    def get_config(self):
322
221
        return self._branch.get_config()
323
222
 
324
 
 
325
223
    def get_revno(self, revid):
326
224
        if revid not in self._revision_info:
327
225
            # ghost parent?
329
227
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
330
228
        return revno_str
331
229
 
332
 
    def get_revision_history(self):
333
 
        return self._full_history
334
 
 
335
230
    def get_revids_from(self, revid_list, start_revid):
336
231
        """
337
232
        Yield the mainline (wrt start_revid) revisions that merged each
359
254
                return
360
255
            revid = parents[0]
361
256
 
362
 
    @with_branch_lock
363
257
    def get_short_revision_history_by_fileid(self, file_id):
364
258
        # wow.  is this really the only way we can get this list?  by
365
259
        # man-handling the weave store directly? :-0
366
260
        # FIXME: would be awesome if we could get, for a folder, the list of
367
261
        # revisions where items within that folder changed.
368
 
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
369
 
        w_revids = w.versions()
370
 
        revids = [r for r in self._full_history if r in w_revids]
371
 
        return revids
 
262
        possible_keys = [(file_id, revid) for revid in self._full_history]
 
263
        existing_keys = self._branch.repository.texts.get_parent_map(possible_keys)
 
264
        return [revid for _, revid in existing_keys.iterkeys()]
372
265
 
373
 
    @with_branch_lock
374
266
    def get_revision_history_since(self, revid_list, date):
375
267
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
376
268
        # so start at midnight on 02-sep.
384
276
        index = -index
385
277
        return revid_list[index:]
386
278
 
387
 
    @with_branch_lock
388
 
    def get_revision_history_matching(self, revid_list, text):
389
 
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
390
 
        z = time.time()
391
 
        # this is going to be painfully slow. :(
392
 
        out = []
393
 
        text = text.lower()
394
 
        for revid in revid_list:
395
 
            change = self.get_changes([ revid ])[0]
396
 
            if text in change.comment.lower():
397
 
                out.append(revid)
398
 
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
399
 
        return out
400
 
 
401
 
    def get_revision_history_matching_indexed(self, revid_list, text):
402
 
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
403
 
        z = time.time()
404
 
        if self._index is None:
405
 
            return self.get_revision_history_matching(revid_list, text)
406
 
        out = self._index.find(text, revid_list)
407
 
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
408
 
        # put them in some coherent order :)
409
 
        out = [r for r in self._full_history if r in out]
410
 
        return out
411
 
 
412
 
    @with_branch_lock
413
279
    def get_search_revid_list(self, query, revid_list):
414
280
        """
415
281
        given a "quick-search" query, try a few obvious possible meanings:
448
314
        if date is not None:
449
315
            if revid_list is None:
450
316
                # if no limit to the query was given, search only the direct-parent path.
451
 
                revid_list = list(self.get_revids_from(None, self._last_revid))
 
317
                revid_list = list(self.get_revids_from(None, self.last_revid))
452
318
            return self.get_revision_history_since(revid_list, date)
453
319
 
454
 
        # check comment fields.
455
 
        if revid_list is None:
456
 
            revid_list = self._full_history
457
 
        return self.get_revision_history_matching_indexed(revid_list, query)
458
 
 
459
320
    revno_re = re.compile(r'^[\d\.]+$')
460
321
    # the date regex are without a final '$' so that queries like
461
322
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
469
330
        if revid is None:
470
331
            return revid
471
332
        if revid == 'head:':
472
 
            return self._last_revid
 
333
            return self.last_revid
473
334
        if self.revno_re.match(revid):
474
335
            revid = self._revno_revid[revid]
475
336
        return revid
476
337
 
477
 
    @with_branch_lock
478
338
    def get_file_view(self, revid, file_id):
479
339
        """
480
340
        Given a revid and optional path, return a (revlist, revid) for
484
344
        If file_id is None, the entire revision history is the list scope.
485
345
        """
486
346
        if revid is None:
487
 
            revid = self._last_revid
 
347
            revid = self.last_revid
488
348
        if file_id is not None:
489
349
            # since revid is 'start_revid', possibly should start the path
490
350
            # tracing from revid... FIXME
494
354
            revlist = list(self.get_revids_from(None, revid))
495
355
        return revlist
496
356
 
497
 
    @with_branch_lock
498
357
    def get_view(self, revid, start_revid, file_id, query=None):
499
358
        """
500
359
        use the URL parameters (revid, start_revid, file_id, and query) to
520
379
        contain vital context for future url navigation.
521
380
        """
522
381
        if start_revid is None:
523
 
            start_revid = self._last_revid
 
382
            start_revid = self.last_revid
524
383
 
525
384
        if query is None:
526
385
            revid_list = self.get_file_view(start_revid, file_id)
538
397
            revid_list = self.get_file_view(start_revid, file_id)
539
398
        else:
540
399
            revid_list = None
541
 
 
542
 
        revid_list = self.get_search_revid_list(query, revid_list)
543
 
        if len(revid_list) > 0:
 
400
        revid_list = search.search_revisions(self._branch, query)
 
401
        if revid_list and len(revid_list) > 0:
544
402
            if revid not in revid_list:
545
403
                revid = revid_list[0]
546
404
            return revid, start_revid, revid_list
547
405
        else:
548
 
            # no results
 
406
            # XXX: This should return a message saying that the search could
 
407
            # not be completed due to either missing the plugin or missing a
 
408
            # search index.
549
409
            return None, None, []
550
410
 
551
 
    @with_branch_lock
552
411
    def get_inventory(self, revid):
553
412
        return self._branch.repository.get_revision_inventory(revid)
554
413
 
555
 
    @with_branch_lock
556
414
    def get_path(self, revid, file_id):
557
415
        if (file_id is None) or (file_id == ''):
558
416
            return ''
561
419
            path = '/' + path
562
420
        return path
563
421
 
564
 
    @with_branch_lock
565
422
    def get_file_id(self, revid, path):
566
423
        if (len(path) > 0) and not path.startswith('/'):
567
424
            path = '/' + path
568
425
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
569
426
 
570
 
 
571
427
    def get_merge_point_list(self, revid):
572
428
        """
573
429
        Return the list of revids that have merged this node.
641
497
                else:
642
498
                    p.branch_nick = '(missing)'
643
499
 
644
 
    @with_branch_lock
645
500
    def get_changes(self, revid_list):
646
501
        """Return a list of changes objects for the given revids.
647
502
 
648
503
        Revisions not present and NULL_REVISION will be ignored.
649
504
        """
650
 
        if self._change_cache is None:
651
 
            changes = self.get_changes_uncached(revid_list)
652
 
        else:
653
 
            changes = self._change_cache.get_changes(revid_list)
 
505
        changes = self.get_changes_uncached(revid_list)
654
506
        if len(changes) == 0:
655
507
            return changes
656
508
 
660
512
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
661
513
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
662
514
            if len(change.parents) > 0:
663
 
                if isinstance(change.parents[0], util.Container):
664
 
                    # old cache stored a potentially-bogus revno
665
 
                    change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
666
 
                else:
667
 
                    change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
 
515
                change.parents = [util.Container(revid=r, 
 
516
                    revno=self.get_revno(r)) for r in change.parents]
668
517
            change.revno = self.get_revno(change.revid)
669
518
 
670
519
        parity = 0
674
523
 
675
524
        return changes
676
525
 
677
 
    # alright, let's profile this sucka. (FIXME remove this eventually...)
678
 
    def _get_changes_profiled(self, revid_list):
679
 
        from loggerhead.lsprof import profile
680
 
        import cPickle
681
 
        ret, stats = profile(self.get_changes_uncached, revid_list)
682
 
        stats.sort()
683
 
        stats.freeze()
684
 
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
685
 
        self.log.info('lsprof complete!')
686
 
        return ret
687
 
 
688
 
    @with_branch_lock
689
 
    @with_bzrlib_read_lock
690
526
    def get_changes_uncached(self, revid_list):
691
527
        # FIXME: deprecated method in getting a null revision
692
528
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
693
529
                            revid_list)
694
 
        repo = self._branch.repository
695
 
        parent_map = repo.get_graph().get_parent_map(revid_list)
 
530
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
696
531
        # We need to return the answer in the same order as the input,
697
532
        # less any ghosts.
698
533
        present_revids = [revid for revid in revid_list
699
534
                          if revid in parent_map]
700
 
        rev_list = repo.get_revisions(present_revids)
 
535
        rev_list = self._branch.repository.get_revisions(present_revids)
701
536
 
702
537
        return [self._change_from_revision(rev) for rev in rev_list]
703
538
 
744
579
        entry = {
745
580
            'revid': revision.revision_id,
746
581
            'date': commit_time,
747
 
            'author': revision.committer,
 
582
            'author': revision.get_apparent_author(),
748
583
            'branch_nick': revision.properties.get('branch-nick', None),
749
584
            'short_comment': short_message,
750
585
            'comment': revision.message,
758
593
 
759
594
        return [self.parse_delta(delta) for delta in delta_list]
760
595
 
761
 
    @with_branch_lock
762
596
    def get_file_changes(self, entries):
763
597
        if self._file_change_cache is None:
764
598
            return self.get_file_changes_uncached(entries)
771
605
        for entry, changes in zip(entries, changes_list):
772
606
            entry.changes = changes
773
607
 
774
 
    @with_branch_lock
775
608
    def get_change_with_diff(self, revid, compare_revid=None):
776
609
        change = self.get_changes([revid])[0]
777
610
 
790
623
 
791
624
        return change
792
625
 
793
 
    @with_branch_lock
794
626
    def get_file(self, file_id, revid):
795
627
        "returns (path, filename, data)"
796
628
        inv = self.get_inventory(revid)
864
696
                old_lineno = lines[0]
865
697
                new_lineno = lines[1]
866
698
            elif line.startswith(' '):
867
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
868
 
                                                 type='context', line=util.fixed_width(line[1:])))
 
699
                chunk.diff.append(util.Container(old_lineno=old_lineno, 
 
700
                                                 new_lineno=new_lineno,
 
701
                                                 type='context', 
 
702
                                                 line=line[1:]))
869
703
                old_lineno += 1
870
704
                new_lineno += 1
871
705
            elif line.startswith('+'):
872
 
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
873
 
                                                 type='insert', line=util.fixed_width(line[1:])))
 
706
                chunk.diff.append(util.Container(old_lineno=None, 
 
707
                                                 new_lineno=new_lineno,
 
708
                                                 type='insert', line=line[1:]))
874
709
                new_lineno += 1
875
710
            elif line.startswith('-'):
876
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
877
 
                                                 type='delete', line=util.fixed_width(line[1:])))
 
711
                chunk.diff.append(util.Container(old_lineno=old_lineno, 
 
712
                                                 new_lineno=None,
 
713
                                                 type='delete', line=line[1:]))
878
714
                old_lineno += 1
879
715
            else:
880
 
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
881
 
                                                 type='unknown', line=util.fixed_width(repr(line))))
 
716
                chunk.diff.append(util.Container(old_lineno=None, 
 
717
                                                 new_lineno=None,
 
718
                                                 type='unknown', 
 
719
                                                 line=repr(line)))
882
720
        if chunk is not None:
883
721
            chunks.append(chunk)
884
722
        return chunks
923
761
            for m in change.changes.modified:
924
762
                m.sbs_chunks = _make_side_by_side(m.chunks)
925
763
 
926
 
    @with_branch_lock
927
764
    def get_filelist(self, inv, file_id, sort_type=None):
928
765
        """
929
766
        return the list of all files (and their attributes) within a given
957
794
            file_list.append(file)
958
795
 
959
796
        if sort_type == 'filename' or sort_type is None:
960
 
            file_list.sort(key=lambda x: x.filename)
 
797
            file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
961
798
        elif sort_type == 'size':
962
799
            file_list.sort(key=lambda x: x.size)
963
800
        elif sort_type == 'date':
964
801
            file_list.sort(key=lambda x: x.change.date)
 
802
        
 
803
        # Always sort by kind to get directories first
 
804
        file_list.sort(key=lambda x: x.kind != 'directory')
965
805
 
966
806
        parity = 0
967
807
        for file in file_list:
973
813
 
974
814
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
975
815
 
976
 
    @with_branch_lock
977
816
    def annotate_file(self, file_id, revid):
978
817
        z = time.time()
979
818
        lineno = 1
1014
853
            lineno += 1
1015
854
 
1016
855
        self.log.debug('annotate: %r secs' % (time.time() - z,))
1017
 
 
1018
 
    @with_branch_lock
1019
 
    def get_bundle(self, revid, compare_revid=None):
1020
 
        if compare_revid is None:
1021
 
            parents = self._revision_graph[revid]
1022
 
            if len(parents) > 0:
1023
 
                compare_revid = parents[0]
1024
 
            else:
1025
 
                compare_revid = None
1026
 
        s = StringIO()
1027
 
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
1028
 
        return s.getvalue()