~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2007-07-04 11:44:22 UTC
  • mfrom: (128.1.39 testing)
  • Revision ID: michael.hudson@canonical.com-20070704114422-77n0yamvzpd3ddhi
merge in a lot of changes from my development branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
141
141
        return False
142
142
 
143
143
 
 
144
def clean_message(message):
 
145
    # clean up a commit message and return it and a short (1-line) version
 
146
    message = message.splitlines()
 
147
    if len(message) == 1:
 
148
        # robey-style 1-line long message
 
149
        message = textwrap.wrap(message[0])
 
150
    elif len(message) == 0:
 
151
        # sometimes a commit may have NO message!
 
152
        message = ['']
 
153
        
 
154
    # make short form of commit message
 
155
    short_message = message[0]
 
156
    if len(short_message) > 80:
 
157
        short_message = short_message[:80] + '...'
 
158
    
 
159
    return message, short_message
 
160
 
 
161
 
 
162
def rich_filename(path, kind):
 
163
    if kind == 'directory':
 
164
        path += '/'
 
165
    if kind == 'symlink':
 
166
        path += '@'
 
167
    return path
 
168
        
 
169
 
 
170
 
144
171
# from bzrlib
145
172
class _RevListToTimestamps(object):
146
173
    """This takes a list of revisions, and allows you to bisect by date"""
171
198
        z = time.time()
172
199
        self = cls()
173
200
        self._branch = branch
174
 
        self._history = branch.revision_history()
175
 
        self._last_revid = self._history[-1]
176
 
        self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
 
201
        self._last_revid = self._branch.last_revision()
 
202
        if self._last_revid is not None:
 
203
            self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
 
204
        else:
 
205
            self._revision_graph = {}
177
206
        
178
207
        if name is None:
179
208
            name = self._branch.nick
184
213
        self._revision_info = {}
185
214
        self._revno_revid = {}
186
215
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
187
 
        count = 0
188
216
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
189
217
            self._full_history.append(revid)
190
218
            revno_str = '.'.join(str(n) for n in revno)
191
219
            self._revno_revid[revno_str] = revid
192
220
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
193
 
            count += 1
194
 
        self._count = count
195
221
 
196
222
        # cache merge info
197
223
        self._where_merged = {}
211
237
 
212
238
    @with_branch_lock
213
239
    def out_of_date(self):
214
 
        if self._branch.revision_history()[-1] != self._last_revid:
 
240
        # the branch may have been upgraded on disk, in which case we're stale.
 
241
        if self._branch.__class__ is not \
 
242
               bzrlib.branch.Branch.open(self._branch.base).__class__:
215
243
            return True
216
 
        return False
 
244
        return self._branch.last_revision() != self._last_revid
217
245
 
218
246
    def use_cache(self, cache):
219
247
        self._change_cache = cache
248
276
    
249
277
    last_revid = property(lambda self: self._last_revid, None, None)
250
278
    
251
 
    count = property(lambda self: self._count, None, None)
252
 
 
253
279
    @with_branch_lock
254
280
    def get_config(self):
255
281
        return self._branch.get_config()
265
291
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
266
292
        return revno_str
267
293
 
268
 
    def get_sequence(self, revid):
269
 
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
270
 
        return seq
271
 
    
272
294
    def get_revision_history(self):
273
295
        return self._full_history
274
296
    
275
 
    def get_revid_sequence(self, revid_list, revid):
276
 
        """
277
 
        given a list of revision ids, return the sequence # of this revid in
278
 
        the list.
279
 
        """
280
 
        seq = 0
281
 
        for r in revid_list:
282
 
            if revid == r:
283
 
                return seq
284
 
            seq += 1
285
 
    
286
297
    def get_revids_from(self, revid_list, revid):
287
298
        """
288
299
        given a list of revision ids, yield revisions in graph order,
406
417
        # if a "revid" is actually a dotted revno, convert it to a revid
407
418
        if revid is None:
408
419
            return revid
 
420
        if revid == 'head:':
 
421
            return self._last_revid
409
422
        if self.revno_re.match(revid):
410
423
            revid = self._revno_revid[revid]
411
424
        return revid
413
426
    @with_branch_lock
414
427
    def get_file_view(self, revid, file_id):
415
428
        """
416
 
        Given an optional revid and optional path, return a (revlist, revid)
417
 
        for navigation through the current scope: from the revid (or the
418
 
        latest revision) back to the original revision.
 
429
        Given a revid and optional path, return a (revlist, revid) for
 
430
        navigation through the current scope: from the revid (or the latest
 
431
        revision) back to the original revision.
419
432
        
420
433
        If file_id is None, the entire revision history is the list scope.
421
 
        If revid is None, the latest revision is used.
422
434
        """
423
435
        if revid is None:
424
436
            revid = self._last_revid
425
437
        if file_id is not None:
426
 
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
427
 
            inv = self._branch.repository.get_revision_inventory(revid)
 
438
            # since revid is 'start_revid', possibly should start the path
 
439
            # tracing from revid... FIXME
428
440
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
429
441
            revlist = list(self.get_revids_from(revlist, revid))
430
442
        else:
431
443
            revlist = list(self.get_revids_from(None, revid))
432
 
        if revid is None:
433
 
            revid = revlist[0]
434
 
        return revlist, revid
 
444
        return revlist
435
445
    
436
446
    @with_branch_lock
437
447
    def get_view(self, revid, start_revid, file_id, query=None):
456
466
        file_id and query are never changed so aren't returned, but they may
457
467
        contain vital context for future url navigation.
458
468
        """
 
469
        if start_revid is None:
 
470
            start_revid = self._last_revid
 
471
 
459
472
        if query is None:
460
 
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
473
            revid_list = self.get_file_view(start_revid, file_id)
461
474
            if revid is None:
462
475
                revid = start_revid
463
476
            if revid not in revid_list:
464
477
                # if the given revid is not in the revlist, use a revlist that
465
478
                # starts at the given revid.
466
 
                revid_list, start_revid = self.get_file_view(revid, file_id)
 
479
                revid_list= self.get_file_view(revid, file_id)
 
480
                start_revid = revid
467
481
            return revid, start_revid, revid_list
468
482
        
469
483
        # potentially limit the search
470
 
        if (start_revid is not None) or (file_id is not None):
471
 
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
484
        if file_id is not None:
 
485
            revid_list = self.get_file_view(start_revid, file_id)
472
486
        else:
473
487
            revid_list = None
474
488
 
494
508
            path = '/' + path
495
509
        return path
496
510
    
497
 
    def get_where_merged(self, revid):
498
 
        try:
499
 
            return self._where_merged[revid]
500
 
        except:
501
 
            return []
 
511
    @with_branch_lock
 
512
    def get_file_id(self, revid, path):
 
513
        if (len(path) > 0) and not path.startswith('/'):
 
514
            path = '/' + path
 
515
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
502
516
    
 
517
 
503
518
    def get_merge_point_list(self, revid):
504
519
        """
505
520
        Return the list of revids that have merged this node.
506
521
        """
507
 
        if revid in self._history:
 
522
        if '.' not in self.get_revno(revid):
508
523
            return []
509
524
        
510
525
        merge_point = []
511
526
        while True:
512
 
            children = self.get_where_merged(revid)
 
527
            children = self._where_merged.get(revid, [])
513
528
            nexts = []
514
529
            for child in children:
515
530
                child_parents = self._revision_graph[child]
561
576
        p_changes = self.get_changes(list(fetch_set))
562
577
        p_change_dict = dict([(c.revid, c) for c in p_changes])
563
578
        for change in changes:
 
579
            # arch-converted branches may not have merged branch info :(
564
580
            for p in change.parents:
565
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
581
                if p.revid in p_change_dict:
 
582
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
583
                else:
 
584
                    p.branch_nick = '(missing)'
566
585
            for p in change.merge_points:
567
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
586
                if p.revid in p_change_dict:
 
587
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
588
                else:
 
589
                    p.branch_nick = '(missing)'
568
590
    
569
591
    @with_branch_lock
570
592
    def get_changes(self, revid_list, get_diffs=False):
571
593
        if self._change_cache is None:
572
 
            changes = self.get_changes_uncached(revid_list, get_diffs)
 
594
            changes = self.get_changes_uncached(revid_list)
573
595
        else:
574
 
            changes = self._change_cache.get_changes(revid_list, get_diffs)
575
 
        if changes is None:
 
596
            changes = self._change_cache.get_changes(revid_list)
 
597
        if len(changes) == 0:
576
598
            return changes
577
599
        
578
600
        # some data needs to be recalculated each time, because it may
579
601
        # change as new revisions are added.
580
 
        for i in xrange(len(revid_list)):
581
 
            revid = revid_list[i]
582
 
            change = changes[i]
583
 
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
 
602
        for change in changes:
 
603
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
584
604
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
 
605
            change.revno = self.get_revno(change.revid)
 
606
            if get_diffs:
 
607
                # this may be time-consuming, but it only happens on the revision page, and only for one revision at a time.
 
608
                if len(change.parents) > 0:
 
609
                    parent_revid = change.parents[0].revid
 
610
                else:
 
611
                    parent_revid = None
 
612
                change.changes.modified = self._parse_diffs(parent_revid, change.revid)
 
613
 
 
614
        parity = 0
 
615
        for change in changes:
 
616
            change.parity = parity
 
617
            parity ^= 1
585
618
        
586
619
        return changes
587
620
 
623
656
        finally:
624
657
            self._branch.repository.unlock()
625
658
    
 
659
    def entry_from_revision(self, revision):
 
660
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
 
661
        
 
662
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
 
663
 
 
664
        message, short_message = clean_message(revision.message)
 
665
 
 
666
        entry = {
 
667
            'revid': revision.revision_id,
 
668
            'date': commit_time,
 
669
            'author': revision.committer,
 
670
            'branch_nick': revision.properties.get('branch-nick', None),
 
671
            'short_comment': short_message,
 
672
            'comment': revision.message,
 
673
            'comment_clean': [util.html_clean(s) for s in message],
 
674
            'parents': parents,
 
675
        }
 
676
        return util.Container(entry)
 
677
 
626
678
    @with_branch_lock
627
679
    @with_bzrlib_read_lock
628
 
    def get_changes_uncached(self, revid_list, get_diffs=False):
629
 
        try:
630
 
            rev_list = self._branch.repository.get_revisions(revid_list)
631
 
        except (KeyError, bzrlib.errors.NoSuchRevision):
632
 
            return None
 
680
    def get_changes_uncached(self, revid_list):
 
681
        done = False
 
682
        while not done:
 
683
            try:
 
684
                rev_list = self._branch.repository.get_revisions(revid_list)
 
685
                done = True
 
686
            except (KeyError, bzrlib.errors.NoSuchRevision), e:
 
687
                # this sometimes happens with arch-converted branches.
 
688
                # i don't know why. :(
 
689
                self.log.debug('No such revision (skipping): %s', e)
 
690
                revid_list.remove(e.revision)
 
691
        if not revid_list:
 
692
            return []
633
693
        
634
694
        delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
635
695
        combined_list = zip(rev_list, delta_list)
636
696
        
637
697
        entries = []
638
698
        for rev, (new_tree, old_tree, delta) in combined_list:
639
 
            commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
640
 
            
641
 
            parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
642
 
    
643
 
            if len(parents) == 0:
644
 
                left_parent = None
645
 
            else:
646
 
                left_parent = rev.parent_ids[0]
647
 
            
648
 
            message = rev.message.splitlines()
649
 
            if len(message) == 1:
650
 
                # robey-style 1-line long message
651
 
                message = textwrap.wrap(message[0])
652
 
            
653
 
            # make short form of commit message
654
 
            short_message = message[0]
655
 
            if len(short_message) > 60:
656
 
                short_message = short_message[:60] + '...'
657
 
    
658
 
            entry = {
659
 
                'revid': rev.revision_id,
660
 
                'revno': self.get_revno(rev.revision_id),
661
 
                'date': commit_time,
662
 
                'author': rev.committer,
663
 
                'branch_nick': rev.properties.get('branch-nick', None),
664
 
                'short_comment': short_message,
665
 
                'comment': rev.message,
666
 
                'comment_clean': [util.html_clean(s) for s in message],
667
 
                'parents': parents,
668
 
                'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
669
 
            }
670
 
            entries.append(util.Container(entry))
 
699
            entry = self.entry_from_revision(rev)
 
700
            entry.changes = self.parse_delta(delta, old_tree, new_tree)
 
701
            entries.append(entry)
671
702
        
672
703
        return entries
673
704
 
 
705
    @with_bzrlib_read_lock
 
706
    def _get_diff(self, revid1, revid2):
 
707
        rev_tree1 = self._branch.repository.revision_tree(revid1)
 
708
        rev_tree2 = self._branch.repository.revision_tree(revid2)
 
709
        delta = rev_tree2.changes_from(rev_tree1)
 
710
        return rev_tree1, rev_tree2, delta
 
711
    
 
712
    def get_diff(self, revid1, revid2):
 
713
        rev_tree1, rev_tree2, delta = self._get_diff(revid1, revid2)
 
714
        entry = self.get_changes([ revid2 ], False)[0]
 
715
        entry.changes = self.parse_delta(delta, True, rev_tree1, rev_tree2)
 
716
        return entry
 
717
    
674
718
    @with_branch_lock
675
719
    def get_file(self, file_id, revid):
676
 
        "returns (filename, data)"
677
 
        inv_entry = self.get_inventory(revid)[file_id]
 
720
        "returns (path, filename, data)"
 
721
        inv = self.get_inventory(revid)
 
722
        inv_entry = inv[file_id]
678
723
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
679
 
        return inv_entry.name, rev_tree.get_file_text(file_id)
 
724
        path = inv.id2path(file_id)
 
725
        if not path.startswith('/'):
 
726
            path = '/' + path
 
727
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
680
728
    
681
 
    @with_branch_lock
682
 
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
 
729
    def _parse_diffs(self, revid1, revid2):
683
730
        """
684
 
        Return a nested data structure containing the changes in a delta::
 
731
        Return a list of processed diffs, in the format::
685
732
        
686
 
            added: list((filename, file_id)),
687
 
            renamed: list((old_filename, new_filename, file_id)),
688
 
            deleted: list((filename, file_id)),
689
 
            modified: list(
 
733
            list(
690
734
                filename: str,
691
735
                file_id: str,
692
736
                chunks: list(
698
742
                    ),
699
743
                ),
700
744
            )
701
 
        
702
 
        if C{get_diffs} is false, the C{chunks} will be omitted.
 
745
        """
 
746
        old_tree, new_tree, delta = self._get_diff(revid1, revid2)
 
747
        process = []
 
748
        out = []
 
749
        
 
750
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
751
            if text_modified:
 
752
                process.append((old_path, new_path, fid, kind))
 
753
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
754
            process.append((path, path, fid, kind))
 
755
        
 
756
        for old_path, new_path, fid, kind in process:
 
757
            old_lines = old_tree.get_file_lines(fid)
 
758
            new_lines = new_tree.get_file_lines(fid)
 
759
            buffer = StringIO()
 
760
            if old_lines != new_lines:
 
761
                try:
 
762
                    bzrlib.diff.internal_diff(old_path, old_lines,
 
763
                                              new_path, new_lines, buffer)
 
764
                except bzrlib.errors.BinaryFile:
 
765
                    diff = ''
 
766
                else:
 
767
                    diff = buffer.getvalue()
 
768
            else:
 
769
                diff = ''
 
770
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
 
771
        
 
772
        return out
 
773
 
 
774
    def _process_diff(self, diff):
 
775
        # doesn't really need to be a method; could be static.
 
776
        chunks = []
 
777
        chunk = None
 
778
        for line in diff.splitlines():
 
779
            if len(line) == 0:
 
780
                continue
 
781
            if line.startswith('+++ ') or line.startswith('--- '):
 
782
                continue
 
783
            if line.startswith('@@ '):
 
784
                # new chunk
 
785
                if chunk is not None:
 
786
                    chunks.append(chunk)
 
787
                chunk = util.Container()
 
788
                chunk.diff = []
 
789
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
 
790
                old_lineno = lines[0]
 
791
                new_lineno = lines[1]
 
792
            elif line.startswith(' '):
 
793
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
794
                                                 type='context', line=util.fixed_width(line[1:])))
 
795
                old_lineno += 1
 
796
                new_lineno += 1
 
797
            elif line.startswith('+'):
 
798
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
799
                                                 type='insert', line=util.fixed_width(line[1:])))
 
800
                new_lineno += 1
 
801
            elif line.startswith('-'):
 
802
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
803
                                                 type='delete', line=util.fixed_width(line[1:])))
 
804
                old_lineno += 1
 
805
            else:
 
806
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
807
                                                 type='unknown', line=util.fixed_width(repr(line))))
 
808
        if chunk is not None:
 
809
            chunks.append(chunk)
 
810
        return chunks
 
811
                
 
812
    @with_branch_lock
 
813
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
 
814
        """
 
815
        Return a nested data structure containing the changes in a delta::
 
816
        
 
817
            added: list((filename, file_id)),
 
818
            renamed: list((old_filename, new_filename, file_id)),
 
819
            deleted: list((filename, file_id)),
 
820
            modified: list(
 
821
                filename: str,
 
822
                file_id: str,
 
823
            )
703
824
        """
704
825
        added = []
705
826
        modified = []
706
827
        renamed = []
707
828
        removed = []
708
829
        
709
 
        def rich_filename(path, kind):
710
 
            if kind == 'directory':
711
 
                path += '/'
712
 
            if kind == 'symlink':
713
 
                path += '@'
714
 
            return path
715
 
        
716
 
        def process_diff(diff):
717
 
            chunks = []
718
 
            chunk = None
719
 
            for line in diff.splitlines():
720
 
                if len(line) == 0:
721
 
                    continue
722
 
                if line.startswith('+++ ') or line.startswith('--- '):
723
 
                    continue
724
 
                if line.startswith('@@ '):
725
 
                    # new chunk
726
 
                    if chunk is not None:
727
 
                        chunks.append(chunk)
728
 
                    chunk = util.Container()
729
 
                    chunk.diff = []
730
 
                    lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
731
 
                    old_lineno = lines[0]
732
 
                    new_lineno = lines[1]
733
 
                elif line.startswith(' '):
734
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
735
 
                                                     type='context', line=util.html_clean(line[1:])))
736
 
                    old_lineno += 1
737
 
                    new_lineno += 1
738
 
                elif line.startswith('+'):
739
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
740
 
                                                     type='insert', line=util.html_clean(line[1:])))
741
 
                    new_lineno += 1
742
 
                elif line.startswith('-'):
743
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
744
 
                                                     type='delete', line=util.html_clean(line[1:])))
745
 
                    old_lineno += 1
746
 
                else:
747
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
748
 
                                                     type='unknown', line=util.html_clean(repr(line))))
749
 
            if chunk is not None:
750
 
                chunks.append(chunk)
751
 
            return chunks
752
 
                    
753
 
        def handle_modify(old_path, new_path, fid, kind):
754
 
            if not get_diffs:
755
 
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
756
 
                return
757
 
            old_lines = old_tree.get_file_lines(fid)
758
 
            new_lines = new_tree.get_file_lines(fid)
759
 
            buffer = StringIO()
760
 
            bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
761
 
            diff = buffer.getvalue()
762
 
            modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
763
 
 
764
830
        for path, fid, kind in delta.added:
765
831
            added.append((rich_filename(path, kind), fid))
766
832
        
767
833
        for path, fid, kind, text_modified, meta_modified in delta.modified:
768
 
            handle_modify(path, path, fid, kind)
 
834
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
769
835
        
770
 
        for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
771
 
            renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
 
836
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
837
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
772
838
            if meta_modified or text_modified:
773
 
                handle_modify(oldpath, newpath, fid, kind)
 
839
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
774
840
        
775
841
        for path, fid, kind in delta.removed:
776
842
            removed.append((rich_filename(path, kind), fid))
785
851
                m.sbs_chunks = _make_side_by_side(m.chunks)
786
852
    
787
853
    @with_branch_lock
788
 
    def get_filelist(self, inv, path, sort_type=None):
 
854
    def get_filelist(self, inv, file_id, sort_type=None):
789
855
        """
790
856
        return the list of all files (and their attributes) within a given
791
857
        path subtree.
792
858
        """
793
 
        while path.endswith('/'):
794
 
            path = path[:-1]
795
 
        if path.startswith('/'):
796
 
            path = path[1:]
797
 
        
798
 
        entries = inv.entries()
799
 
        
800
 
        fetch_set = set()
801
 
        for filepath, entry in entries:
802
 
            fetch_set.add(entry.revision)
803
 
        change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
804
 
        
 
859
 
 
860
        dir_ie = inv[file_id]
 
861
        path = inv.id2path(file_id)
805
862
        file_list = []
806
 
        for filepath, entry in entries:
807
 
            if posixpath.dirname(filepath) != path:
808
 
                continue
809
 
            filename = posixpath.basename(filepath)
810
 
            rich_filename = filename
 
863
 
 
864
        for filename, entry in dir_ie.children.iteritems():
811
865
            pathname = filename
812
866
            if entry.kind == 'directory':
813
867
                pathname += '/'
814
 
            
815
 
            # last change:
 
868
 
816
869
            revid = entry.revision
817
 
            change = change_dict[revid]
818
 
            
819
 
            file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
820
 
                                  pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
 
870
            if self._change_cache:
 
871
                timestamp = self.get_changes([revid])[0].date
 
872
            else:
 
873
                revision = self._branch.repository.get_revision(revid)
 
874
                timestamp = datetime.datetime.fromtimestamp(revision.timestamp)
 
875
 
 
876
            change = util.Container(date=timestamp,
 
877
                                    revno=self.get_revno(revid))
 
878
 
 
879
            file = util.Container(
 
880
                filename=filename, executable=entry.executable, kind=entry.kind,
 
881
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
 
882
                revid=revid, change=change)
821
883
            file_list.append(file)
822
 
        
823
 
        if sort_type == 'filename':
 
884
 
 
885
        if sort_type == 'filename' or sort_type is None:
824
886
            file_list.sort(key=lambda x: x.filename)
825
887
        elif sort_type == 'size':
826
888
            file_list.sort(key=lambda x: x.size)
827
889
        elif sort_type == 'date':
828
890
            file_list.sort(key=lambda x: x.change.date)
829
 
        
 
891
 
830
892
        parity = 0
831
893
        for file in file_list:
832
894
            file.parity = parity
835
897
        return file_list
836
898
 
837
899
 
838
 
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
 
900
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
839
901
 
840
902
    @with_branch_lock
841
903
    def annotate_file(self, file_id, revid):
857
919
            if self._BADCHARS_RE.match(text):
858
920
                # bail out; this isn't displayable text
859
921
                yield util.Container(parity=0, lineno=1, status='same',
860
 
                                     text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
 
922
                                     text='(This is a binary file.)',
861
923
                                     change=util.Container())
862
924
                return
863
925
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
875
937
                trunc_revno = change.revno
876
938
                if len(trunc_revno) > 10:
877
939
                    trunc_revno = trunc_revno[:9] + '...'
878
 
                
 
940
 
879
941
            yield util.Container(parity=parity, lineno=lineno, status=status,
880
 
                                 change=change, text=util.html_clean(text))
 
942
                                 change=change, text=util.fixed_width(text))
881
943
            lineno += 1
882
944
        
883
945
        self.log.debug('annotate: %r secs' % (time.time() - z,))
884
946
 
885
947
    @with_branch_lock
886
948
    @with_bzrlib_read_lock
887
 
    def get_bundle(self, revid):
888
 
        parents = self._revision_graph[revid]
889
 
        if len(parents) > 0:
890
 
            parent_revid = parents[0]
891
 
        else:
892
 
            parent_revid = None
 
949
    def get_bundle(self, revid, compare_revid=None):
 
950
        if compare_revid is None:
 
951
            parents = self._revision_graph[revid]
 
952
            if len(parents) > 0:
 
953
                compare_revid = parents[0]
 
954
            else:
 
955
                compare_revid = None
893
956
        s = StringIO()
894
 
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, parent_revid, s)
 
957
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
895
958
        return s.getvalue()
896
959