~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2008-03-06 04:54:39 UTC
  • mfrom: (148.1.3 changelog-order-aaaargh)
  • Revision ID: launchpad@pqm.canonical.com-20080306045439-qe3j5ryuhb7bmqr8
[r=jamesh] fix the order of the changelog view when the revision cache is not used

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
 
28
28
 
29
29
import bisect
30
 
import cgi
31
30
import datetime
32
31
import logging
33
 
import os
34
 
import posixpath
35
32
import re
36
 
import shelve
37
 
import sys
38
33
import textwrap
39
34
import threading
40
35
import time
44
39
from loggerhead.util import decorator
45
40
 
46
41
import bzrlib
47
 
import bzrlib.annotate
48
42
import bzrlib.branch
49
43
import bzrlib.bundle.serializer
50
 
import bzrlib.decorators
51
44
import bzrlib.diff
52
45
import bzrlib.errors
53
46
import bzrlib.progress
54
 
import bzrlib.textfile
 
47
import bzrlib.revision
55
48
import bzrlib.tsort
56
49
import bzrlib.ui
57
50
 
58
51
 
59
52
with_branch_lock = util.with_lock('_lock', 'branch')
60
53
 
 
54
 
61
55
@decorator
62
56
def with_bzrlib_read_lock(unbound):
63
57
    def bzrlib_read_locked(self, *args, **kw):
100
94
    """
101
95
    turn a normal unified-style diff (post-processed by parse_delta) into a
102
96
    side-by-side diff structure.  the new structure is::
103
 
    
 
97
 
104
98
        chunks: list(
105
99
            diff: list(
106
100
                old_lineno: int,
142
136
 
143
137
 
144
138
def clean_message(message):
145
 
    # clean up a commit message and return it and a short (1-line) version
 
139
    """Clean up a commit message and return it and a short (1-line) version.
 
140
 
 
141
    Commit messages that are long single lines are reflowed using the textwrap
 
142
    module (Robey, the original author of this code, apparently favored this
 
143
    style of message).
 
144
    """
146
145
    message = message.splitlines()
 
146
 
147
147
    if len(message) == 1:
148
 
        # robey-style 1-line long message
149
148
        message = textwrap.wrap(message[0])
150
 
        
151
 
    # make short form of commit message
 
149
 
 
150
    if len(message) == 0:
 
151
        # We can end up where when (a) the commit message was empty or (b)
 
152
        # when the message consisted entirely of whitespace, in which case
 
153
        # textwrap.wrap() returns an empty list.
 
154
        return [''], ''
 
155
 
 
156
    # Make short form of commit message.
152
157
    short_message = message[0]
153
 
    if len(short_message) > 60:
154
 
        short_message = short_message[:60] + '...'
155
 
    
 
158
    if len(short_message) > 80:
 
159
        short_message = short_message[:80] + '...'
 
160
 
156
161
    return message, short_message
157
162
 
158
163
 
 
164
def rich_filename(path, kind):
 
165
    if kind == 'directory':
 
166
        path += '/'
 
167
    if kind == 'symlink':
 
168
        path += '@'
 
169
    return path
 
170
 
 
171
 
 
172
 
159
173
# from bzrlib
160
174
class _RevListToTimestamps(object):
161
175
    """This takes a list of revisions, and allows you to bisect by date"""
175
189
 
176
190
 
177
191
class History (object):
178
 
    
 
192
 
179
193
    def __init__(self):
180
194
        self._change_cache = None
 
195
        self._file_change_cache = None
181
196
        self._index = None
182
197
        self._lock = threading.RLock()
183
 
    
 
198
 
184
199
    @classmethod
185
200
    def from_branch(cls, branch, name=None):
186
201
        z = time.time()
187
202
        self = cls()
188
203
        self._branch = branch
189
 
        self._history = branch.revision_history()
190
 
        self._last_revid = self._history[-1]
 
204
        self._last_revid = self._branch.last_revision()
191
205
        self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
192
 
        
 
206
 
193
207
        if name is None:
194
208
            name = self._branch.nick
195
209
        self._name = name
196
210
        self.log = logging.getLogger('loggerhead.%s' % (name,))
197
 
        
 
211
 
198
212
        self._full_history = []
199
213
        self._revision_info = {}
200
214
        self._revno_revid = {}
201
 
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
202
 
        count = 0
 
215
        if bzrlib.revision.is_null(self._last_revid):
 
216
            self._merge_sort = []
 
217
        else:
 
218
            self._merge_sort = bzrlib.tsort.merge_sort(
 
219
                self._revision_graph, self._last_revid, generate_revno=True)
203
220
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
204
221
            self._full_history.append(revid)
205
222
            revno_str = '.'.join(str(n) for n in revno)
206
223
            self._revno_revid[revno_str] = revid
207
224
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
208
 
            count += 1
209
 
        self._count = count
210
 
 
211
225
        # cache merge info
212
226
        self._where_merged = {}
213
227
        for revid in self._revision_graph.keys():
214
 
            if not revid in self._full_history: 
 
228
            if self._revision_info[revid][2] == 0:
215
229
                continue
216
230
            for parent in self._revision_graph[revid]:
217
231
                self._where_merged.setdefault(parent, set()).add(revid)
218
232
 
219
233
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
220
234
        return self
221
 
    
 
235
 
222
236
    @classmethod
223
237
    def from_folder(cls, path, name=None):
224
238
        b = bzrlib.branch.Branch.open(path)
225
 
        return cls.from_branch(b, name)
 
239
        b.lock_read()
 
240
        try:
 
241
            return cls.from_branch(b, name)
 
242
        finally:
 
243
            b.unlock()
226
244
 
227
245
    @with_branch_lock
228
246
    def out_of_date(self):
229
 
        if self._branch.revision_history()[-1] != self._last_revid:
230
 
            return True
231
 
        return False
 
247
        # the branch may have been upgraded on disk, in which case we're stale.
 
248
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
 
249
        if self._branch.__class__ is not \
 
250
               newly_opened.__class__:
 
251
            return True
 
252
        if self._branch.repository.__class__ is not \
 
253
               newly_opened.repository.__class__:
 
254
            return True
 
255
        return self._branch.last_revision() != self._last_revid
232
256
 
233
257
    def use_cache(self, cache):
234
258
        self._change_cache = cache
235
 
    
 
259
 
 
260
    def use_file_cache(self, cache):
 
261
        self._file_change_cache = cache
 
262
 
236
263
    def use_search_index(self, index):
237
264
        self._index = index
238
265
 
 
266
    @property
 
267
    def has_revisions(self):
 
268
        return not bzrlib.revision.is_null(self.last_revid)
 
269
 
239
270
    @with_branch_lock
240
271
    def detach(self):
241
272
        # called when a new history object needs to be created, because the
254
285
        if self._change_cache is None:
255
286
            return
256
287
        self._change_cache.flush()
257
 
    
 
288
 
258
289
    def check_rebuild(self):
259
290
        if self._change_cache is not None:
260
291
            self._change_cache.check_rebuild()
261
292
        if self._index is not None:
262
293
            self._index.check_rebuild()
263
 
    
 
294
 
264
295
    last_revid = property(lambda self: self._last_revid, None, None)
265
 
    
266
 
    count = property(lambda self: self._count, None, None)
267
296
 
268
297
    @with_branch_lock
269
298
    def get_config(self):
270
299
        return self._branch.get_config()
271
 
    
272
 
    @with_branch_lock
273
 
    def get_revision(self, revid):
274
 
        return self._branch.repository.get_revision(revid)
275
 
    
 
300
 
276
301
    def get_revno(self, revid):
277
302
        if revid not in self._revision_info:
278
303
            # ghost parent?
280
305
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
281
306
        return revno_str
282
307
 
283
 
    def get_sequence(self, revid):
284
 
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
285
 
        return seq
286
 
    
287
308
    def get_revision_history(self):
288
309
        return self._full_history
289
 
    
290
 
    def get_revid_sequence(self, revid_list, revid):
291
 
        """
292
 
        given a list of revision ids, return the sequence # of this revid in
293
 
        the list.
294
 
        """
295
 
        seq = 0
296
 
        for r in revid_list:
297
 
            if revid == r:
298
 
                return seq
299
 
            seq += 1
300
 
    
 
310
 
301
311
    def get_revids_from(self, revid_list, revid):
302
312
        """
303
313
        given a list of revision ids, yield revisions in graph order,
313
323
            if len(parents) == 0:
314
324
                return
315
325
            revid = parents[0]
316
 
    
 
326
 
317
327
    @with_branch_lock
318
328
    def get_short_revision_history_by_fileid(self, file_id):
319
329
        # wow.  is this really the only way we can get this list?  by
338
348
        revid_list.reverse()
339
349
        index = -index
340
350
        return revid_list[index:]
341
 
    
 
351
 
342
352
    @with_branch_lock
343
353
    def get_revision_history_matching(self, revid_list, text):
344
354
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
363
373
        # put them in some coherent order :)
364
374
        out = [r for r in self._full_history if r in out]
365
375
        return out
366
 
    
 
376
 
367
377
    @with_branch_lock
368
378
    def get_search_revid_list(self, query, revid_list):
369
379
        """
370
380
        given a "quick-search" query, try a few obvious possible meanings:
371
 
        
 
381
 
372
382
            - revision id or # ("128.1.3")
373
383
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
374
384
            - comment text as a fallback
378
388
        # FIXME: there is some silliness in this action.  we have to look up
379
389
        # all the relevant changes (time-consuming) only to return a list of
380
390
        # revids which will be used to fetch a set of changes again.
381
 
        
 
391
 
382
392
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
383
393
        revid = self.fix_revid(query)
384
394
        if revid is not None:
 
395
            if isinstance(revid, unicode):
 
396
                revid = revid.encode('utf-8')
385
397
            changes = self.get_changes([ revid ])
386
398
            if (changes is not None) and (len(changes) > 0):
387
399
                return [ revid ]
388
 
        
 
400
 
389
401
        date = None
390
402
        m = self.us_date_re.match(query)
391
403
        if m is not None:
403
415
                # if no limit to the query was given, search only the direct-parent path.
404
416
                revid_list = list(self.get_revids_from(None, self._last_revid))
405
417
            return self.get_revision_history_since(revid_list, date)
406
 
        
 
418
 
407
419
        # check comment fields.
408
420
        if revid_list is None:
409
421
            revid_list = self._full_history
410
422
        return self.get_revision_history_matching_indexed(revid_list, query)
411
 
    
 
423
 
412
424
    revno_re = re.compile(r'^[\d\.]+$')
413
425
    # the date regex are without a final '$' so that queries like
414
426
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
421
433
        # if a "revid" is actually a dotted revno, convert it to a revid
422
434
        if revid is None:
423
435
            return revid
 
436
        if revid == 'head:':
 
437
            return self._last_revid
424
438
        if self.revno_re.match(revid):
425
439
            revid = self._revno_revid[revid]
426
440
        return revid
427
 
    
 
441
 
428
442
    @with_branch_lock
429
443
    def get_file_view(self, revid, file_id):
430
444
        """
431
 
        Given an optional revid and optional path, return a (revlist, revid)
432
 
        for navigation through the current scope: from the revid (or the
433
 
        latest revision) back to the original revision.
434
 
        
 
445
        Given a revid and optional path, return a (revlist, revid) for
 
446
        navigation through the current scope: from the revid (or the latest
 
447
        revision) back to the original revision.
 
448
 
435
449
        If file_id is None, the entire revision history is the list scope.
436
 
        If revid is None, the latest revision is used.
437
450
        """
438
451
        if revid is None:
439
452
            revid = self._last_revid
440
453
        if file_id is not None:
441
 
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
442
 
            inv = self._branch.repository.get_revision_inventory(revid)
 
454
            # since revid is 'start_revid', possibly should start the path
 
455
            # tracing from revid... FIXME
443
456
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
444
457
            revlist = list(self.get_revids_from(revlist, revid))
445
458
        else:
446
459
            revlist = list(self.get_revids_from(None, revid))
447
 
        if revid is None:
448
 
            revid = revlist[0]
449
 
        return revlist, revid
450
 
    
 
460
        return revlist
 
461
 
451
462
    @with_branch_lock
452
463
    def get_view(self, revid, start_revid, file_id, query=None):
453
464
        """
454
465
        use the URL parameters (revid, start_revid, file_id, and query) to
455
466
        determine the revision list we're viewing (start_revid, file_id, query)
456
467
        and where we are in it (revid).
457
 
        
458
 
        if a query is given, we're viewing query results.
459
 
        if a file_id is given, we're viewing revisions for a specific file.
460
 
        if a start_revid is given, we're viewing the branch from a
461
 
            specific revision up the tree.
462
 
        (these may be combined to view revisions for a specific file, from
463
 
            a specific revision, with a specific search query.)
464
 
            
465
 
        returns a new (revid, start_revid, revid_list, scan_list) where:
466
 
        
 
468
 
 
469
            - if a query is given, we're viewing query results.
 
470
            - if a file_id is given, we're viewing revisions for a specific
 
471
              file.
 
472
            - if a start_revid is given, we're viewing the branch from a
 
473
              specific revision up the tree.
 
474
 
 
475
        these may be combined to view revisions for a specific file, from
 
476
        a specific revision, with a specific search query.
 
477
 
 
478
        returns a new (revid, start_revid, revid_list) where:
 
479
 
467
480
            - revid: current position within the view
468
481
            - start_revid: starting revision of this view
469
482
            - revid_list: list of revision ids for this view
470
 
        
 
483
 
471
484
        file_id and query are never changed so aren't returned, but they may
472
485
        contain vital context for future url navigation.
473
486
        """
 
487
        if start_revid is None:
 
488
            start_revid = self._last_revid
 
489
 
474
490
        if query is None:
475
 
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
491
            revid_list = self.get_file_view(start_revid, file_id)
476
492
            if revid is None:
477
493
                revid = start_revid
478
494
            if revid not in revid_list:
479
495
                # if the given revid is not in the revlist, use a revlist that
480
496
                # starts at the given revid.
481
 
                revid_list, start_revid = self.get_file_view(revid, file_id)
 
497
                revid_list= self.get_file_view(revid, file_id)
 
498
                start_revid = revid
482
499
            return revid, start_revid, revid_list
483
 
        
 
500
 
484
501
        # potentially limit the search
485
 
        if (start_revid is not None) or (file_id is not None):
486
 
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
502
        if file_id is not None:
 
503
            revid_list = self.get_file_view(start_revid, file_id)
487
504
        else:
488
505
            revid_list = None
489
506
 
508
525
        if (len(path) > 0) and not path.startswith('/'):
509
526
            path = '/' + path
510
527
        return path
511
 
    
512
 
    def get_where_merged(self, revid):
513
 
        try:
514
 
            return self._where_merged[revid]
515
 
        except:
516
 
            return []
517
 
    
 
528
 
 
529
    @with_branch_lock
 
530
    def get_file_id(self, revid, path):
 
531
        if (len(path) > 0) and not path.startswith('/'):
 
532
            path = '/' + path
 
533
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
 
534
 
 
535
 
518
536
    def get_merge_point_list(self, revid):
519
537
        """
520
538
        Return the list of revids that have merged this node.
521
539
        """
522
 
        if revid in self._history:
 
540
        if '.' not in self.get_revno(revid):
523
541
            return []
524
 
        
 
542
 
525
543
        merge_point = []
526
544
        while True:
527
 
            children = self.get_where_merged(revid)
 
545
            children = self._where_merged.get(revid, [])
528
546
            nexts = []
529
547
            for child in children:
530
548
                child_parents = self._revision_graph[child]
544
562
                merge_point.extend(merge_point_next)
545
563
 
546
564
            revid = nexts[0]
547
 
            
 
565
 
548
566
    def simplify_merge_point_list(self, revids):
549
567
        """if a revision is already merged, don't show further merge points"""
550
568
        d = {}
576
594
        p_changes = self.get_changes(list(fetch_set))
577
595
        p_change_dict = dict([(c.revid, c) for c in p_changes])
578
596
        for change in changes:
 
597
            # arch-converted branches may not have merged branch info :(
579
598
            for p in change.parents:
580
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
599
                if p.revid in p_change_dict:
 
600
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
601
                else:
 
602
                    p.branch_nick = '(missing)'
581
603
            for p in change.merge_points:
582
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
583
 
    
 
604
                if p.revid in p_change_dict:
 
605
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
606
                else:
 
607
                    p.branch_nick = '(missing)'
 
608
 
584
609
    @with_branch_lock
585
 
    def get_changes(self, revid_list, get_diffs=False):
 
610
    def get_changes(self, revid_list):
 
611
        """Return a list of changes objects for the given revids.
 
612
 
 
613
        Revisions not present and NULL_REVISION will be ignored.
 
614
        """
586
615
        if self._change_cache is None:
587
 
            changes = self.get_changes_uncached(revid_list, get_diffs)
 
616
            changes = self.get_changes_uncached(revid_list)
588
617
        else:
589
 
            changes = self._change_cache.get_changes(revid_list, get_diffs)
590
 
        if changes is None:
 
618
            changes = self._change_cache.get_changes(revid_list)
 
619
        if len(changes) == 0:
591
620
            return changes
592
 
        
 
621
 
593
622
        # some data needs to be recalculated each time, because it may
594
623
        # change as new revisions are added.
595
 
        for i in xrange(len(revid_list)):
596
 
            revid = revid_list[i]
597
 
            change = changes[i]
598
 
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
 
624
        for change in changes:
 
625
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
599
626
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
600
 
        
 
627
            if len(change.parents) > 0:
 
628
                if isinstance(change.parents[0], util.Container):
 
629
                    # old cache stored a potentially-bogus revno
 
630
                    change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
 
631
                else:
 
632
                    change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
 
633
            change.revno = self.get_revno(change.revid)
 
634
 
 
635
        parity = 0
 
636
        for change in changes:
 
637
            change.parity = parity
 
638
            parity ^= 1
 
639
 
601
640
        return changes
602
641
 
603
 
    # alright, let's profile this sucka.
604
 
    def _get_changes_profiled(self, revid_list, get_diffs=False):
 
642
    # alright, let's profile this sucka. (FIXME remove this eventually...)
 
643
    def _get_changes_profiled(self, revid_list):
605
644
        from loggerhead.lsprof import profile
606
645
        import cPickle
607
 
        ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
 
646
        ret, stats = profile(self.get_changes_uncached, revid_list)
608
647
        stats.sort()
609
648
        stats.freeze()
610
649
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
611
650
        self.log.info('lsprof complete!')
612
651
        return ret
613
652
 
 
653
    @with_branch_lock
 
654
    @with_bzrlib_read_lock
 
655
    def get_changes_uncached(self, revid_list):
 
656
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
 
657
                            revid_list)
 
658
        repo = self._branch.repository
 
659
        parent_map = repo.get_graph().get_parent_map(revid_list)
 
660
        # We need to return the answer in the same order as the input,
 
661
        # less any ghosts.
 
662
        present_revids = [revid for revid in revid_list
 
663
                          if revid in parent_map]
 
664
        rev_list = repo.get_revisions(present_revids)
 
665
 
 
666
        return [self._change_from_revision(rev) for rev in rev_list]
 
667
 
614
668
    def _get_deltas_for_revisions_with_trees(self, revisions):
615
 
        """Produce a generator of revision deltas.
616
 
        
 
669
        """Produce a list of revision deltas.
 
670
 
617
671
        Note that the input is a sequence of REVISIONS, not revision_ids.
618
672
        Trees will be held in memory until the generator exits.
619
673
        Each delta is relative to the revision's lefthand predecessor.
 
674
        (This is copied from bzrlib.)
620
675
        """
621
676
        required_trees = set()
622
677
        for revision in revisions:
623
 
            required_trees.add(revision.revision_id)
624
 
            required_trees.update(revision.parent_ids[:1])
625
 
        trees = dict((t.get_revision_id(), t) for 
 
678
            required_trees.add(revision.revid)
 
679
            required_trees.update([p.revid for p in revision.parents[:1]])
 
680
        trees = dict((t.get_revision_id(), t) for
626
681
                     t in self._branch.repository.revision_trees(required_trees))
627
682
        ret = []
628
683
        self._branch.repository.lock_read()
629
684
        try:
630
685
            for revision in revisions:
631
 
                if not revision.parent_ids:
632
 
                    old_tree = self._branch.repository.revision_tree(None)
 
686
                if not revision.parents:
 
687
                    old_tree = self._branch.repository.revision_tree(
 
688
                        bzrlib.revision.NULL_REVISION)
633
689
                else:
634
 
                    old_tree = trees[revision.parent_ids[0]]
635
 
                tree = trees[revision.revision_id]
636
 
                ret.append((tree, old_tree, tree.changes_from(old_tree)))
 
690
                    old_tree = trees[revision.parents[0].revid]
 
691
                tree = trees[revision.revid]
 
692
                ret.append(tree.changes_from(old_tree))
637
693
            return ret
638
694
        finally:
639
695
            self._branch.repository.unlock()
640
 
    
641
 
    def entry_from_revision(self, revision):
 
696
 
 
697
    def _change_from_revision(self, revision):
 
698
        """
 
699
        Given a bzrlib Revision, return a processed "change" for use in
 
700
        templates.
 
701
        """
642
702
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
643
 
        
 
703
 
644
704
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
645
705
 
646
 
        if len(parents) == 0:
647
 
            left_parent = None
648
 
        else:
649
 
            left_parent = revision.parent_ids[0]
650
 
        
651
706
        message, short_message = clean_message(revision.message)
652
707
 
653
708
        entry = {
654
709
            'revid': revision.revision_id,
655
 
            'revno': self.get_revno(revision.revision_id),
656
710
            'date': commit_time,
657
711
            'author': revision.committer,
658
712
            'branch_nick': revision.properties.get('branch-nick', None),
659
713
            'short_comment': short_message,
660
714
            'comment': revision.message,
661
715
            'comment_clean': [util.html_clean(s) for s in message],
662
 
            'parents': parents,
 
716
            'parents': revision.parent_ids,
663
717
        }
664
718
        return util.Container(entry)
665
719
 
666
 
    @with_branch_lock
667
 
    @with_bzrlib_read_lock
668
 
    def get_changes_uncached(self, revid_list, get_diffs=False):
669
 
        try:
670
 
            rev_list = self._branch.repository.get_revisions(revid_list)
671
 
        except (KeyError, bzrlib.errors.NoSuchRevision):
672
 
            return None
673
 
        
674
 
        delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
675
 
        combined_list = zip(rev_list, delta_list)
676
 
        
677
 
        entries = []
678
 
        for rev, (new_tree, old_tree, delta) in combined_list:
679
 
            entry = self.entry_from_revision(rev)
680
 
            entry.changes = self.parse_delta(delta, get_diffs, old_tree, new_tree)
681
 
            entries.append(entry)
682
 
        
683
 
        return entries
684
 
 
685
 
    @with_bzrlib_read_lock
686
 
    def _get_diff(self, revid1, revid2):
687
 
        rev_tree1 = self._branch.repository.revision_tree(revid1)
688
 
        rev_tree2 = self._branch.repository.revision_tree(revid2)
 
720
    def get_file_changes_uncached(self, entries):
 
721
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
 
722
 
 
723
        return [self.parse_delta(delta) for delta in delta_list]
 
724
 
 
725
    @with_branch_lock
 
726
    def get_file_changes(self, entries):
 
727
        if self._file_change_cache is None:
 
728
            return self.get_file_changes_uncached(entries)
 
729
        else:
 
730
            return self._file_change_cache.get_file_changes(entries)
 
731
 
 
732
    def add_changes(self, entries):
 
733
        changes_list = self.get_file_changes(entries)
 
734
 
 
735
        for entry, changes in zip(entries, changes_list):
 
736
            entry.changes = changes
 
737
 
 
738
    @with_branch_lock
 
739
    def get_change_with_diff(self, revid, compare_revid=None):
 
740
        change = self.get_changes([revid])[0]
 
741
 
 
742
        if compare_revid is None:
 
743
            if change.parents:
 
744
                compare_revid = change.parents[0].revid
 
745
            else:
 
746
                compare_revid = 'null:'
 
747
 
 
748
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
 
749
        rev_tree2 = self._branch.repository.revision_tree(revid)
689
750
        delta = rev_tree2.changes_from(rev_tree1)
690
 
        return rev_tree1, rev_tree2, delta
691
 
    
692
 
    def get_diff(self, revid1, revid2):
693
 
        rev_tree1, rev_tree2, delta = self._get_diff(revid1, revid2)
694
 
        entry = self.get_changes([ revid2 ], False)[0]
695
 
        entry.changes = self.parse_delta(delta, True, rev_tree1, rev_tree2)
696
 
        return entry
697
 
    
 
751
 
 
752
        change.changes = self.parse_delta(delta)
 
753
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
754
 
 
755
        return change
 
756
 
698
757
    @with_branch_lock
699
758
    def get_file(self, file_id, revid):
700
759
        "returns (path, filename, data)"
705
764
        if not path.startswith('/'):
706
765
            path = '/' + path
707
766
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
708
 
    
709
 
    @with_branch_lock
710
 
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
 
767
 
 
768
    def _parse_diffs(self, old_tree, new_tree, delta):
711
769
        """
712
 
        Return a nested data structure containing the changes in a delta::
713
 
        
714
 
            added: list((filename, file_id)),
715
 
            renamed: list((old_filename, new_filename, file_id)),
716
 
            deleted: list((filename, file_id)),
717
 
            modified: list(
 
770
        Return a list of processed diffs, in the format::
 
771
 
 
772
            list(
718
773
                filename: str,
719
774
                file_id: str,
720
775
                chunks: list(
726
781
                    ),
727
782
                ),
728
783
            )
729
 
        
730
 
        if C{get_diffs} is false, the C{chunks} will be omitted.
 
784
        """
 
785
        process = []
 
786
        out = []
 
787
 
 
788
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
789
            if text_modified:
 
790
                process.append((old_path, new_path, fid, kind))
 
791
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
792
            process.append((path, path, fid, kind))
 
793
 
 
794
        for old_path, new_path, fid, kind in process:
 
795
            old_lines = old_tree.get_file_lines(fid)
 
796
            new_lines = new_tree.get_file_lines(fid)
 
797
            buffer = StringIO()
 
798
            if old_lines != new_lines:
 
799
                try:
 
800
                    bzrlib.diff.internal_diff(old_path, old_lines,
 
801
                                              new_path, new_lines, buffer)
 
802
                except bzrlib.errors.BinaryFile:
 
803
                    diff = ''
 
804
                else:
 
805
                    diff = buffer.getvalue()
 
806
            else:
 
807
                diff = ''
 
808
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
 
809
 
 
810
        return out
 
811
 
 
812
    def _process_diff(self, diff):
 
813
        # doesn't really need to be a method; could be static.
 
814
        chunks = []
 
815
        chunk = None
 
816
        for line in diff.splitlines():
 
817
            if len(line) == 0:
 
818
                continue
 
819
            if line.startswith('+++ ') or line.startswith('--- '):
 
820
                continue
 
821
            if line.startswith('@@ '):
 
822
                # new chunk
 
823
                if chunk is not None:
 
824
                    chunks.append(chunk)
 
825
                chunk = util.Container()
 
826
                chunk.diff = []
 
827
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
 
828
                old_lineno = lines[0]
 
829
                new_lineno = lines[1]
 
830
            elif line.startswith(' '):
 
831
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
832
                                                 type='context', line=util.fixed_width(line[1:])))
 
833
                old_lineno += 1
 
834
                new_lineno += 1
 
835
            elif line.startswith('+'):
 
836
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
837
                                                 type='insert', line=util.fixed_width(line[1:])))
 
838
                new_lineno += 1
 
839
            elif line.startswith('-'):
 
840
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
841
                                                 type='delete', line=util.fixed_width(line[1:])))
 
842
                old_lineno += 1
 
843
            else:
 
844
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
845
                                                 type='unknown', line=util.fixed_width(repr(line))))
 
846
        if chunk is not None:
 
847
            chunks.append(chunk)
 
848
        return chunks
 
849
 
 
850
    def parse_delta(self, delta):
 
851
        """
 
852
        Return a nested data structure containing the changes in a delta::
 
853
 
 
854
            added: list((filename, file_id)),
 
855
            renamed: list((old_filename, new_filename, file_id)),
 
856
            deleted: list((filename, file_id)),
 
857
            modified: list(
 
858
                filename: str,
 
859
                file_id: str,
 
860
            )
731
861
        """
732
862
        added = []
733
863
        modified = []
734
864
        renamed = []
735
865
        removed = []
736
 
        
737
 
        def rich_filename(path, kind):
738
 
            if kind == 'directory':
739
 
                path += '/'
740
 
            if kind == 'symlink':
741
 
                path += '@'
742
 
            return path
743
 
        
744
 
        def process_diff(diff):
745
 
            chunks = []
746
 
            chunk = None
747
 
            for line in diff.splitlines():
748
 
                if len(line) == 0:
749
 
                    continue
750
 
                if line.startswith('+++ ') or line.startswith('--- '):
751
 
                    continue
752
 
                if line.startswith('@@ '):
753
 
                    # new chunk
754
 
                    if chunk is not None:
755
 
                        chunks.append(chunk)
756
 
                    chunk = util.Container()
757
 
                    chunk.diff = []
758
 
                    lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
759
 
                    old_lineno = lines[0]
760
 
                    new_lineno = lines[1]
761
 
                elif line.startswith(' '):
762
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
763
 
                                                     type='context', line=util.html_clean(line[1:])))
764
 
                    old_lineno += 1
765
 
                    new_lineno += 1
766
 
                elif line.startswith('+'):
767
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
768
 
                                                     type='insert', line=util.html_clean(line[1:])))
769
 
                    new_lineno += 1
770
 
                elif line.startswith('-'):
771
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
772
 
                                                     type='delete', line=util.html_clean(line[1:])))
773
 
                    old_lineno += 1
774
 
                else:
775
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
776
 
                                                     type='unknown', line=util.html_clean(repr(line))))
777
 
            if chunk is not None:
778
 
                chunks.append(chunk)
779
 
            return chunks
780
 
                    
781
 
        def handle_modify(old_path, new_path, fid, kind):
782
 
            if not get_diffs:
783
 
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
784
 
                return
785
 
            old_lines = old_tree.get_file_lines(fid)
786
 
            new_lines = new_tree.get_file_lines(fid)
787
 
            buffer = StringIO()
788
 
            bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
789
 
            diff = buffer.getvalue()
790
 
            modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
791
866
 
792
867
        for path, fid, kind in delta.added:
793
868
            added.append((rich_filename(path, kind), fid))
794
 
        
 
869
 
795
870
        for path, fid, kind, text_modified, meta_modified in delta.modified:
796
 
            handle_modify(path, path, fid, kind)
797
 
        
798
 
        for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
799
 
            renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
 
871
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
 
872
 
 
873
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
874
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
800
875
            if meta_modified or text_modified:
801
 
                handle_modify(oldpath, newpath, fid, kind)
802
 
        
 
876
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
877
 
803
878
        for path, fid, kind in delta.removed:
804
879
            removed.append((rich_filename(path, kind), fid))
805
 
        
 
880
 
806
881
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
807
882
 
808
883
    @staticmethod
811
886
        for change in changes:
812
887
            for m in change.changes.modified:
813
888
                m.sbs_chunks = _make_side_by_side(m.chunks)
814
 
    
 
889
 
815
890
    @with_branch_lock
816
 
    def get_filelist(self, inv, path, sort_type=None):
 
891
    def get_filelist(self, inv, file_id, sort_type=None):
817
892
        """
818
893
        return the list of all files (and their attributes) within a given
819
894
        path subtree.
820
895
        """
821
 
        while path.endswith('/'):
822
 
            path = path[:-1]
823
 
        if path.startswith('/'):
824
 
            path = path[1:]
825
 
        
826
 
        entries = inv.entries()
827
 
        
828
 
        fetch_set = set()
829
 
        for filepath, entry in entries:
830
 
            fetch_set.add(entry.revision)
831
 
        change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
832
 
        
 
896
 
 
897
        dir_ie = inv[file_id]
 
898
        path = inv.id2path(file_id)
833
899
        file_list = []
834
 
        for filepath, entry in entries:
835
 
            if posixpath.dirname(filepath) != path:
836
 
                continue
837
 
            filename = posixpath.basename(filepath)
838
 
            rich_filename = filename
 
900
 
 
901
        revid_set = set()
 
902
 
 
903
        for filename, entry in dir_ie.children.iteritems():
 
904
            revid_set.add(entry.revision)
 
905
 
 
906
        change_dict = {}
 
907
        for change in self.get_changes(list(revid_set)):
 
908
            change_dict[change.revid] = change
 
909
 
 
910
        for filename, entry in dir_ie.children.iteritems():
839
911
            pathname = filename
840
912
            if entry.kind == 'directory':
841
913
                pathname += '/'
842
 
            
843
 
            # last change:
 
914
 
844
915
            revid = entry.revision
845
 
            change = change_dict[revid]
846
 
            
847
 
            file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
848
 
                                  pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
 
916
 
 
917
            file = util.Container(
 
918
                filename=filename, executable=entry.executable, kind=entry.kind,
 
919
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
 
920
                revid=revid, change=change_dict[revid])
849
921
            file_list.append(file)
850
 
        
851
 
        if sort_type == 'filename':
 
922
 
 
923
        if sort_type == 'filename' or sort_type is None:
852
924
            file_list.sort(key=lambda x: x.filename)
853
925
        elif sort_type == 'size':
854
926
            file_list.sort(key=lambda x: x.size)
855
927
        elif sort_type == 'date':
856
928
            file_list.sort(key=lambda x: x.change.date)
857
 
        
 
929
 
858
930
        parity = 0
859
931
        for file in file_list:
860
932
            file.parity = parity
863
935
        return file_list
864
936
 
865
937
 
866
 
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
 
938
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
867
939
 
868
940
    @with_branch_lock
869
941
    def annotate_file(self, file_id, revid):
870
942
        z = time.time()
871
943
        lineno = 1
872
944
        parity = 0
873
 
        
 
945
 
874
946
        file_revid = self.get_inventory(revid)[file_id].revision
875
947
        oldvalues = None
876
 
        
 
948
 
877
949
        # because we cache revision metadata ourselves, it's actually much
878
950
        # faster to call 'annotate_iter' on the weave directly than it is to
879
951
        # ask bzrlib to annotate for us.
880
952
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
881
 
        
 
953
 
882
954
        revid_set = set()
883
955
        for line_revid, text in w.annotate_iter(file_revid):
884
956
            revid_set.add(line_revid)
885
957
            if self._BADCHARS_RE.match(text):
886
958
                # bail out; this isn't displayable text
887
959
                yield util.Container(parity=0, lineno=1, status='same',
888
 
                                     text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
 
960
                                     text='(This is a binary file.)',
889
961
                                     change=util.Container())
890
962
                return
891
963
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
892
 
        
 
964
 
893
965
        last_line_revid = None
894
966
        for line_revid, text in w.annotate_iter(file_revid):
895
967
            if line_revid == last_line_revid:
903
975
                trunc_revno = change.revno
904
976
                if len(trunc_revno) > 10:
905
977
                    trunc_revno = trunc_revno[:9] + '...'
906
 
                
 
978
 
907
979
            yield util.Container(parity=parity, lineno=lineno, status=status,
908
 
                                 change=change, text=util.html_clean(text))
 
980
                                 change=change, text=util.fixed_width(text))
909
981
            lineno += 1
910
 
        
 
982
 
911
983
        self.log.debug('annotate: %r secs' % (time.time() - z,))
912
984
 
913
985
    @with_branch_lock
914
 
    @with_bzrlib_read_lock
915
986
    def get_bundle(self, revid, compare_revid=None):
916
987
        if compare_revid is None:
917
988
            parents = self._revision_graph[revid]
922
993
        s = StringIO()
923
994
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
924
995
        return s.getvalue()
925