~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:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd. 
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
29
27
 
30
28
 
31
29
import bisect
 
30
import cgi
32
31
import datetime
33
32
import logging
 
33
import os
 
34
import posixpath
34
35
import re
 
36
import shelve
 
37
import sys
35
38
import textwrap
36
39
import threading
37
40
import time
38
41
from StringIO import StringIO
39
42
 
40
 
from loggerhead import search
41
43
from loggerhead import util
42
 
from loggerhead.wholehistory import compute_whole_history_data
 
44
from loggerhead.util import decorator
43
45
 
44
46
import bzrlib
 
47
import bzrlib.annotate
45
48
import bzrlib.branch
 
49
import bzrlib.bundle.serializer
 
50
import bzrlib.decorators
46
51
import bzrlib.diff
47
52
import bzrlib.errors
48
53
import bzrlib.progress
49
54
import bzrlib.revision
 
55
import bzrlib.textfile
50
56
import bzrlib.tsort
51
57
import bzrlib.ui
52
58
 
 
59
 
 
60
with_branch_lock = util.with_lock('_lock', 'branch')
 
61
 
 
62
 
53
63
# bzrlib's UIFactory is not thread-safe
54
64
uihack = threading.local()
55
65
 
93
103
    out_chunk_list = []
94
104
    for chunk in chunk_list:
95
105
        line_list = []
96
 
        wrap_char = '<wbr/>'
97
106
        delete_list, insert_list = [], []
98
107
        for line in chunk.diff:
99
 
            # Add <wbr/> every X characters so we can wrap properly
100
 
            wrap_line = re.findall(r'.{%d}|.+$' % 78, line.line)
101
 
            wrap_lines = [util.html_clean(_line) for _line in wrap_line]
102
 
            wrapped_line = wrap_char.join(wrap_lines)
103
 
 
104
108
            if line.type == 'context':
105
109
                if len(delete_list) or len(insert_list):
106
 
                    _process_side_by_side_buffers(line_list, delete_list, 
107
 
                                                  insert_list)
 
110
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
108
111
                    delete_list, insert_list = [], []
109
 
                line_list.append(util.Container(old_lineno=line.old_lineno, 
110
 
                                                new_lineno=line.new_lineno,
111
 
                                                old_line=wrapped_line, 
112
 
                                                new_line=wrapped_line,
113
 
                                                old_type=line.type, 
114
 
                                                new_type=line.type))
 
112
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
 
113
                                                old_line=line.line, new_line=line.line,
 
114
                                                old_type=line.type, new_type=line.type))
115
115
            elif line.type == 'delete':
116
 
                delete_list.append((line.old_lineno, wrapped_line, line.type))
 
116
                delete_list.append((line.old_lineno, line.line, line.type))
117
117
            elif line.type == 'insert':
118
 
                insert_list.append((line.new_lineno, wrapped_line, line.type))
 
118
                insert_list.append((line.new_lineno, line.line, line.type))
119
119
        if len(delete_list) or len(insert_list):
120
120
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
121
121
        out_chunk_list.append(util.Container(diff=line_list))
150
150
 
151
151
    # Make short form of commit message.
152
152
    short_message = message[0]
153
 
    if len(short_message) > 60:
154
 
        short_message = short_message[:60] + '...'
 
153
    if len(short_message) > 80:
 
154
        short_message = short_message[:80] + '...'
155
155
 
156
156
    return message, short_message
157
157
 
184
184
 
185
185
 
186
186
class History (object):
187
 
    """Decorate a branch to provide information for rendering.
188
 
 
189
 
    History objects are expected to be short lived -- when serving a request
190
 
    for a particular branch, open it, read-lock it, wrap a History object
191
 
    around it, serve the request, throw the History object away, unlock the
192
 
    branch and throw it away.
193
 
 
194
 
    :ivar _file_change_cache: xx
195
 
    """
196
 
 
197
 
    def __init__(self, branch, whole_history_data_cache):
198
 
        assert branch.is_locked(), (
199
 
            "Can only construct a History object with a read-locked branch.")
 
187
 
 
188
    def __init__(self):
 
189
        self._change_cache = None
200
190
        self._file_change_cache = None
 
191
        self._index = None
 
192
        self._lock = threading.RLock()
 
193
 
 
194
    @classmethod
 
195
    def from_branch(cls, branch, name=None):
 
196
        z = time.time()
 
197
        self = cls()
201
198
        self._branch = branch
202
 
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
203
 
 
204
 
        self.last_revid = branch.last_revision()
205
 
 
206
 
        whole_history_data = whole_history_data_cache.get(self.last_revid)
207
 
        if whole_history_data is None:
208
 
            whole_history_data = compute_whole_history_data(branch)
209
 
            whole_history_data_cache[self.last_revid] = whole_history_data
210
 
 
211
 
        (self._revision_graph, self._full_history, self._revision_info,
212
 
         self._revno_revid, self._merge_sort, self._where_merged
213
 
         ) = whole_history_data
 
199
        self._last_revid = self._branch.last_revision()
 
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
 
 
210
        self._full_history = []
 
211
        self._revision_info = {}
 
212
        self._revno_revid = {}
 
213
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
 
214
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
 
215
            self._full_history.append(revid)
 
216
            revno_str = '.'.join(str(n) for n in revno)
 
217
            self._revno_revid[revno_str] = revid
 
218
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
 
219
 
 
220
        # cache merge info
 
221
        self._where_merged = {}
 
222
        for revid in self._revision_graph.keys():
 
223
            if not revid in self._full_history:
 
224
                continue
 
225
            for parent in self._revision_graph[revid]:
 
226
                self._where_merged.setdefault(parent, set()).add(revid)
 
227
 
 
228
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
 
229
        return self
 
230
 
 
231
    @classmethod
 
232
    def from_folder(cls, path, name=None):
 
233
        b = bzrlib.branch.Branch.open(path)
 
234
        return cls.from_branch(b, name)
 
235
 
 
236
    @with_branch_lock
 
237
    def out_of_date(self):
 
238
        # the branch may have been upgraded on disk, in which case we're stale.
 
239
        if self._branch.__class__ is not \
 
240
               bzrlib.branch.Branch.open(self._branch.base).__class__:
 
241
            return True
 
242
        return self._branch.last_revision() != self._last_revid
 
243
 
 
244
    def use_cache(self, cache):
 
245
        self._change_cache = cache
214
246
 
215
247
    def use_file_cache(self, cache):
216
248
        self._file_change_cache = cache
217
249
 
218
 
    @property
219
 
    def has_revisions(self):
220
 
        return not bzrlib.revision.is_null(self.last_revid)
221
 
 
 
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()
 
277
 
 
278
    last_revid = property(lambda self: self._last_revid, None, None)
 
279
 
 
280
    @with_branch_lock
222
281
    def get_config(self):
223
282
        return self._branch.get_config()
224
283
 
229
288
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
230
289
        return revno_str
231
290
 
232
 
    def get_revids_from(self, revid_list, start_revid):
233
 
        """
234
 
        Yield the mainline (wrt start_revid) revisions that merged each
235
 
        revid in revid_list.
236
 
        """
237
 
        if revid_list is None:
238
 
            revid_list = self._full_history
239
 
        revid_set = set(revid_list)
240
 
        revid = start_revid
241
 
        def introduced_revisions(revid):
242
 
            r = set([revid])
243
 
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
244
 
            i = seq + 1
245
 
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
246
 
                r.add(self._merge_sort[i][1])
247
 
                i += 1
248
 
            return r
249
 
        while 1:
250
 
            if bzrlib.revision.is_null(revid):
251
 
                return
252
 
            if introduced_revisions(revid) & revid_set:
 
291
    def get_revision_history(self):
 
292
        return self._full_history
 
293
 
 
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):
253
302
                yield revid
 
303
            if not self._revision_graph.has_key(revid):
 
304
                return
254
305
            parents = self._revision_graph[revid]
255
306
            if len(parents) == 0:
256
307
                return
257
308
            revid = parents[0]
258
309
 
 
310
    @with_branch_lock
259
311
    def get_short_revision_history_by_fileid(self, file_id):
260
312
        # wow.  is this really the only way we can get this list?  by
261
313
        # man-handling the weave store directly? :-0
262
314
        # FIXME: would be awesome if we could get, for a folder, the list of
263
315
        # revisions where items within that folder changed.
264
 
        possible_keys = [(file_id, revid) for revid in self._full_history]
265
 
        existing_keys = self._branch.repository.texts.get_parent_map(possible_keys)
266
 
        return [revid for _, revid in existing_keys.iterkeys()]
 
316
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
317
        w_revids = w.versions()
 
318
        revids = [r for r in self._full_history if r in w_revids]
 
319
        return revids
267
320
 
 
321
    @with_branch_lock
268
322
    def get_revision_history_since(self, revid_list, date):
269
323
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
270
324
        # so start at midnight on 02-sep.
278
332
        index = -index
279
333
        return revid_list[index:]
280
334
 
 
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
281
361
    def get_search_revid_list(self, query, revid_list):
282
362
        """
283
363
        given a "quick-search" query, try a few obvious possible meanings:
316
396
        if date is not None:
317
397
            if revid_list is None:
318
398
                # if no limit to the query was given, search only the direct-parent path.
319
 
                revid_list = list(self.get_revids_from(None, self.last_revid))
 
399
                revid_list = list(self.get_revids_from(None, self._last_revid))
320
400
            return self.get_revision_history_since(revid_list, date)
321
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
 
322
407
    revno_re = re.compile(r'^[\d\.]+$')
323
408
    # the date regex are without a final '$' so that queries like
324
409
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
332
417
        if revid is None:
333
418
            return revid
334
419
        if revid == 'head:':
335
 
            return self.last_revid
 
420
            return self._last_revid
336
421
        if self.revno_re.match(revid):
337
422
            revid = self._revno_revid[revid]
338
423
        return revid
339
424
 
 
425
    @with_branch_lock
340
426
    def get_file_view(self, revid, file_id):
341
427
        """
342
428
        Given a revid and optional path, return a (revlist, revid) for
346
432
        If file_id is None, the entire revision history is the list scope.
347
433
        """
348
434
        if revid is None:
349
 
            revid = self.last_revid
 
435
            revid = self._last_revid
350
436
        if file_id is not None:
351
437
            # since revid is 'start_revid', possibly should start the path
352
438
            # tracing from revid... FIXME
356
442
            revlist = list(self.get_revids_from(None, revid))
357
443
        return revlist
358
444
 
 
445
    @with_branch_lock
359
446
    def get_view(self, revid, start_revid, file_id, query=None):
360
447
        """
361
448
        use the URL parameters (revid, start_revid, file_id, and query) to
362
449
        determine the revision list we're viewing (start_revid, file_id, query)
363
450
        and where we are in it (revid).
364
451
 
365
 
            - if a query is given, we're viewing query results.
366
 
            - if a file_id is given, we're viewing revisions for a specific
367
 
              file.
368
 
            - if a start_revid is given, we're viewing the branch from a
369
 
              specific revision up the tree.
370
 
 
371
 
        these may be combined to view revisions for a specific file, from
372
 
        a specific revision, with a specific search query.
373
 
 
374
 
        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:
375
460
 
376
461
            - revid: current position within the view
377
462
            - start_revid: starting revision of this view
381
466
        contain vital context for future url navigation.
382
467
        """
383
468
        if start_revid is None:
384
 
            start_revid = self.last_revid
 
469
            start_revid = self._last_revid
385
470
 
386
471
        if query is None:
387
472
            revid_list = self.get_file_view(start_revid, file_id)
390
475
            if revid not in revid_list:
391
476
                # if the given revid is not in the revlist, use a revlist that
392
477
                # starts at the given revid.
393
 
                revid_list = self.get_file_view(revid, file_id)
 
478
                revid_list= self.get_file_view(revid, file_id)
394
479
                start_revid = revid
395
480
            return revid, start_revid, revid_list
396
481
 
399
484
            revid_list = self.get_file_view(start_revid, file_id)
400
485
        else:
401
486
            revid_list = None
402
 
        revid_list = search.search_revisions(self._branch, query)
403
 
        if revid_list and len(revid_list) > 0:
 
487
 
 
488
        revid_list = self.get_search_revid_list(query, revid_list)
 
489
        if len(revid_list) > 0:
404
490
            if revid not in revid_list:
405
491
                revid = revid_list[0]
406
492
            return revid, start_revid, revid_list
407
493
        else:
408
 
            # XXX: This should return a message saying that the search could
409
 
            # not be completed due to either missing the plugin or missing a
410
 
            # search index.
 
494
            # no results
411
495
            return None, None, []
412
496
 
 
497
    @with_branch_lock
413
498
    def get_inventory(self, revid):
414
499
        return self._branch.repository.get_revision_inventory(revid)
415
500
 
 
501
    @with_branch_lock
416
502
    def get_path(self, revid, file_id):
417
503
        if (file_id is None) or (file_id == ''):
418
504
            return ''
421
507
            path = '/' + path
422
508
        return path
423
509
 
 
510
    @with_branch_lock
424
511
    def get_file_id(self, revid, path):
425
512
        if (len(path) > 0) and not path.startswith('/'):
426
513
            path = '/' + path
427
514
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
428
515
 
 
516
 
429
517
    def get_merge_point_list(self, revid):
430
518
        """
431
519
        Return the list of revids that have merged this node.
499
587
                else:
500
588
                    p.branch_nick = '(missing)'
501
589
 
 
590
    @with_branch_lock
502
591
    def get_changes(self, revid_list):
503
 
        """Return a list of changes objects for the given revids.
504
 
 
505
 
        Revisions not present and NULL_REVISION will be ignored.
506
 
        """
507
 
        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)
508
596
        if len(changes) == 0:
509
597
            return changes
510
598
 
513
601
        for change in changes:
514
602
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
515
603
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
516
 
            if len(change.parents) > 0:
517
 
                change.parents = [util.Container(revid=r, 
518
 
                    revno=self.get_revno(r)) for r in change.parents]
519
604
            change.revno = self.get_revno(change.revid)
520
605
 
521
606
        parity = 0
525
610
 
526
611
        return changes
527
612
 
528
 
    def get_changes_uncached(self, revid_list):
529
 
        # FIXME: deprecated method in getting a null revision
530
 
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
531
 
                            revid_list)
532
 
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
533
 
        # We need to return the answer in the same order as the input,
534
 
        # less any ghosts.
535
 
        present_revids = [revid for revid in revid_list
536
 
                          if revid in parent_map]
537
 
        rev_list = self._branch.repository.get_revisions(present_revids)
538
 
 
539
 
        return [self._change_from_revision(rev) for rev in rev_list]
540
 
 
541
 
    def _get_deltas_for_revisions_with_trees(self, revisions):
542
 
        """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.
543
626
 
544
627
        Note that the input is a sequence of REVISIONS, not revision_ids.
545
628
        Trees will be held in memory until the generator exits.
546
629
        Each delta is relative to the revision's lefthand predecessor.
547
 
        (This is copied from bzrlib.)
548
630
        """
549
631
        required_trees = set()
550
 
        for revision in revisions:
551
 
            required_trees.add(revision.revid)
552
 
            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]])
553
635
        trees = dict((t.get_revision_id(), t) for
554
636
                     t in self._branch.repository.revision_trees(required_trees))
555
637
        ret = []
556
638
        self._branch.repository.lock_read()
557
639
        try:
558
 
            for revision in revisions:
559
 
                if not revision.parents:
 
640
            for entry in entries:
 
641
                if not entry.parents:
560
642
                    old_tree = self._branch.repository.revision_tree(
561
643
                        bzrlib.revision.NULL_REVISION)
562
644
                else:
563
 
                    old_tree = trees[revision.parents[0].revid]
564
 
                tree = trees[revision.revid]
 
645
                    old_tree = trees[entry.parents[0].revid]
 
646
                tree = trees[entry.revid]
565
647
                ret.append(tree.changes_from(old_tree))
566
648
            return ret
567
649
        finally:
568
650
            self._branch.repository.unlock()
569
651
 
570
 
    def _change_from_revision(self, revision):
571
 
        """
572
 
        Given a bzrlib Revision, return a processed "change" for use in
573
 
        templates.
574
 
        """
 
652
    def entry_from_revision(self, revision):
575
653
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
576
654
 
577
655
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
581
659
        entry = {
582
660
            'revid': revision.revision_id,
583
661
            'date': commit_time,
584
 
            'author': revision.get_apparent_author(),
 
662
            'author': revision.committer,
585
663
            'branch_nick': revision.properties.get('branch-nick', None),
586
664
            'short_comment': short_message,
587
665
            'comment': revision.message,
588
666
            'comment_clean': [util.html_clean(s) for s in message],
589
 
            'parents': revision.parent_ids,
 
667
            'parents': parents,
590
668
        }
591
669
        return util.Container(entry)
592
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
 
593
692
    def get_file_changes_uncached(self, entries):
594
693
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
595
694
 
596
695
        return [self.parse_delta(delta) for delta in delta_list]
597
696
 
 
697
    @with_branch_lock
598
698
    def get_file_changes(self, entries):
599
699
        if self._file_change_cache is None:
600
700
            return self.get_file_changes_uncached(entries)
607
707
        for entry, changes in zip(entries, changes_list):
608
708
            entry.changes = changes
609
709
 
 
710
    @with_branch_lock
610
711
    def get_change_with_diff(self, revid, compare_revid=None):
611
 
        change = self.get_changes([revid])[0]
 
712
        entry = self.get_changes([revid])[0]
612
713
 
613
714
        if compare_revid is None:
614
 
            if change.parents:
615
 
                compare_revid = change.parents[0].revid
 
715
            if entry.parents:
 
716
                compare_revid = entry.parents[0].revid
616
717
            else:
617
718
                compare_revid = 'null:'
618
719
 
620
721
        rev_tree2 = self._branch.repository.revision_tree(revid)
621
722
        delta = rev_tree2.changes_from(rev_tree1)
622
723
 
623
 
        change.changes = self.parse_delta(delta)
624
 
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
625
 
 
626
 
        return change
627
 
 
 
724
        entry.changes = self.parse_delta(delta)
 
725
 
 
726
        entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
727
 
 
728
        return entry
 
729
 
 
730
    @with_branch_lock
628
731
    def get_file(self, file_id, revid):
629
732
        "returns (path, filename, data)"
630
733
        inv = self.get_inventory(revid)
675
778
                    diff = buffer.getvalue()
676
779
            else:
677
780
                diff = ''
678
 
            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)))
679
782
 
680
783
        return out
681
784
 
698
801
                old_lineno = lines[0]
699
802
                new_lineno = lines[1]
700
803
            elif line.startswith(' '):
701
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, 
702
 
                                                 new_lineno=new_lineno,
703
 
                                                 type='context', 
704
 
                                                 line=line[1:]))
 
804
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
805
                                                 type='context', line=util.fixed_width(line[1:])))
705
806
                old_lineno += 1
706
807
                new_lineno += 1
707
808
            elif line.startswith('+'):
708
 
                chunk.diff.append(util.Container(old_lineno=None, 
709
 
                                                 new_lineno=new_lineno,
710
 
                                                 type='insert', line=line[1:]))
 
809
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
810
                                                 type='insert', line=util.fixed_width(line[1:])))
711
811
                new_lineno += 1
712
812
            elif line.startswith('-'):
713
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, 
714
 
                                                 new_lineno=None,
715
 
                                                 type='delete', line=line[1:]))
 
813
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
814
                                                 type='delete', line=util.fixed_width(line[1:])))
716
815
                old_lineno += 1
717
816
            else:
718
 
                chunk.diff.append(util.Container(old_lineno=None, 
719
 
                                                 new_lineno=None,
720
 
                                                 type='unknown', 
721
 
                                                 line=repr(line)))
 
817
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
818
                                                 type='unknown', line=util.fixed_width(repr(line))))
722
819
        if chunk is not None:
723
820
            chunks.append(chunk)
724
821
        return chunks
763
860
            for m in change.changes.modified:
764
861
                m.sbs_chunks = _make_side_by_side(m.chunks)
765
862
 
 
863
    @with_branch_lock
766
864
    def get_filelist(self, inv, file_id, sort_type=None):
767
865
        """
768
866
        return the list of all files (and their attributes) within a given
796
894
            file_list.append(file)
797
895
 
798
896
        if sort_type == 'filename' or sort_type is None:
799
 
            file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
 
897
            file_list.sort(key=lambda x: x.filename)
800
898
        elif sort_type == 'size':
801
899
            file_list.sort(key=lambda x: x.size)
802
900
        elif sort_type == 'date':
803
901
            file_list.sort(key=lambda x: x.change.date)
804
 
        
805
 
        # Always sort by kind to get directories first
806
 
        file_list.sort(key=lambda x: x.kind != 'directory')
807
902
 
808
903
        parity = 0
809
904
        for file in file_list:
815
910
 
816
911
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
817
912
 
 
913
    @with_branch_lock
818
914
    def annotate_file(self, file_id, revid):
819
915
        z = time.time()
820
916
        lineno = 1
822
918
 
823
919
        file_revid = self.get_inventory(revid)[file_id].revision
824
920
        oldvalues = None
825
 
        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
 
826
927
        revid_set = set()
827
 
 
828
 
        for line_revid, text in tree.annotate_iter(file_id):
 
928
        for line_revid, text in w.annotate_iter(file_revid):
829
929
            revid_set.add(line_revid)
830
930
            if self._BADCHARS_RE.match(text):
831
931
                # bail out; this isn't displayable text
833
933
                                     text='(This is a binary file.)',
834
934
                                     change=util.Container())
835
935
                return
836
 
        change_cache = dict([(c.revid, c) \
837
 
                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))])
838
937
 
839
938
        last_line_revid = None
840
 
        for line_revid, text in tree.annotate_iter(file_id):
 
939
        for line_revid, text in w.annotate_iter(file_revid):
841
940
            if line_revid == last_line_revid:
842
941
                # remember which lines have a new revno and which don't
843
942
                status = 'same'
855
954
            lineno += 1
856
955
 
857
956
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
957
 
 
958
    @with_branch_lock
 
959
    def get_bundle(self, revid, compare_revid=None):
 
960
        if compare_revid is None:
 
961
            parents = self._revision_graph[revid]
 
962
            if len(parents) > 0:
 
963
                compare_revid = parents[0]
 
964
            else:
 
965
                compare_revid = None
 
966
        s = StringIO()
 
967
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
 
968
        return s.getvalue()
 
969