~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Matt Nordhoff
  • Date: 2009-10-17 09:28:42 UTC
  • mto: This revision was merged to the branch mainline in revision 406.
  • Revision ID: mnordhoff@mattnordhoff.com-20091017092842-o9oefagbqdh7ud4t
Remove trailing whitespace in the non-Python files too

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
 
2
# Copyright (C) 2008, 2009 Canonical Ltd.
3
3
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
4
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
33
33
import logging
34
34
import re
35
35
import textwrap
36
 
import threading
37
 
import time
38
 
import urllib
39
 
from StringIO import StringIO
 
36
 
 
37
import bzrlib.branch
 
38
import bzrlib.delta
 
39
import bzrlib.errors
 
40
import bzrlib.revision
40
41
 
41
42
from loggerhead import search
42
43
from loggerhead import util
43
44
from loggerhead.wholehistory import compute_whole_history_data
44
45
 
45
 
import bzrlib
46
 
import bzrlib.branch
47
 
import bzrlib.diff
48
 
import bzrlib.errors
49
 
import bzrlib.progress
50
 
import bzrlib.revision
51
 
import bzrlib.textfile
52
 
import bzrlib.tsort
53
 
import bzrlib.ui
54
 
 
55
 
# bzrlib's UIFactory is not thread-safe
56
 
uihack = threading.local()
57
 
 
58
 
 
59
 
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
60
 
 
61
 
    def nested_progress_bar(self):
62
 
        if getattr(uihack, '_progress_bar_stack', None) is None:
63
 
            pbs = bzrlib.progress.ProgressBarStack(
64
 
                      klass=bzrlib.progress.DummyProgress)
65
 
            uihack._progress_bar_stack = pbs
66
 
        return uihack._progress_bar_stack.get_nested()
67
 
 
68
 
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
69
46
 
70
47
def is_branch(folder):
71
48
    try:
82
59
    module (Robey, the original author of this code, apparently favored this
83
60
    style of message).
84
61
    """
85
 
    message = message.splitlines()
 
62
    message = message.lstrip().splitlines()
86
63
 
87
64
    if len(message) == 1:
88
65
        message = textwrap.wrap(message[0])
109
86
    return path
110
87
 
111
88
 
112
 
# from bzrlib
113
 
 
114
 
 
115
89
class _RevListToTimestamps(object):
116
90
    """This takes a list of revisions, and allows you to bisect by date"""
117
91
 
129
103
    def __len__(self):
130
104
        return len(self.revid_list)
131
105
 
132
 
 
133
 
class History (object):
 
106
class FileChangeReporter(object):
 
107
 
 
108
    def __init__(self, old_inv, new_inv):
 
109
        self.added = []
 
110
        self.modified = []
 
111
        self.renamed = []
 
112
        self.removed = []
 
113
        self.text_changes = []
 
114
        self.old_inv = old_inv
 
115
        self.new_inv = new_inv
 
116
 
 
117
    def revid(self, inv, file_id):
 
118
        try:
 
119
            return inv[file_id].revision
 
120
        except bzrlib.errors.NoSuchId:
 
121
            return 'null:'
 
122
 
 
123
    def report(self, file_id, paths, versioned, renamed, modified,
 
124
               exe_change, kind):
 
125
        if modified not in ('unchanged', 'kind changed'):
 
126
            if versioned == 'removed':
 
127
                filename = rich_filename(paths[0], kind[0])
 
128
            else:
 
129
                filename = rich_filename(paths[1], kind[1])
 
130
            self.text_changes.append(util.Container(
 
131
                filename=filename, file_id=file_id,
 
132
                old_revision=self.revid(self.old_inv, file_id),
 
133
                new_revision=self.revid(self.new_inv, file_id)))
 
134
        if versioned == 'added':
 
135
            self.added.append(util.Container(
 
136
                filename=rich_filename(paths[1], kind),
 
137
                file_id=file_id, kind=kind[1]))
 
138
        elif versioned == 'removed':
 
139
            self.removed.append(util.Container(
 
140
                filename=rich_filename(paths[0], kind),
 
141
                file_id=file_id, kind=kind[0]))
 
142
        elif renamed:
 
143
            self.renamed.append(util.Container(
 
144
                old_filename=rich_filename(paths[0], kind[0]),
 
145
                new_filename=rich_filename(paths[1], kind[1]),
 
146
                file_id=file_id,
 
147
                text_modified=modified == 'modified'))
 
148
        else:
 
149
            self.modified.append(util.Container(
 
150
                filename=rich_filename(paths[1], kind),
 
151
                file_id=file_id))
 
152
 
 
153
 
 
154
class RevInfoMemoryCache(object):
 
155
    """A store that validates values against the revids they were stored with.
 
156
 
 
157
    We use a unique key for each branch.
 
158
 
 
159
    The reason for not just using the revid as the key is so that when a new
 
160
    value is provided for a branch, we replace the old value used for the
 
161
    branch.
 
162
 
 
163
    There is another implementation of the same interface in
 
164
    loggerhead.changecache.RevInfoDiskCache.
 
165
    """
 
166
 
 
167
    def __init__(self, cache):
 
168
        self._cache = cache
 
169
 
 
170
    def get(self, key, revid):
 
171
        """Return the data associated with `key`, subject to a revid check.
 
172
 
 
173
        If a value was stored under `key`, with the same revid, return it.
 
174
        Otherwise return None.
 
175
        """
 
176
        cached = self._cache.get(key)
 
177
        if cached is None:
 
178
            return None
 
179
        stored_revid, data = cached
 
180
        if revid == stored_revid:
 
181
            return data
 
182
        else:
 
183
            return None
 
184
 
 
185
    def set(self, key, revid, data):
 
186
        """Store `data` under `key`, to be checked against `revid` on get().
 
187
        """
 
188
        self._cache[key] = (revid, data)
 
189
 
 
190
 
 
191
class History(object):
134
192
    """Decorate a branch to provide information for rendering.
135
193
 
136
194
    History objects are expected to be short lived -- when serving a request
138
196
    around it, serve the request, throw the History object away, unlock the
139
197
    branch and throw it away.
140
198
 
141
 
    :ivar _file_change_cache: xx
 
199
    :ivar _file_change_cache: An object that caches information about the
 
200
        files that changed between two revisions.
 
201
    :ivar _rev_info: A list of information about revisions.  This is by far
 
202
        the most cryptic data structure in loggerhead.  At the top level, it
 
203
        is a list of 3-tuples [(merge-info, where-merged, parents)].
 
204
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
 
205
        like a merged sorted list, but the revno is stringified.
 
206
        `where-merged` is a tuple of revisions that have this revision as a
 
207
        non-lefthand parent.  Finally, `parents` is just the usual list of
 
208
        parents of this revision.
 
209
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
 
210
        the information about it in _rev_info.
 
211
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
 
212
        ids.
142
213
    """
143
214
 
144
 
    def __init__(self, branch, whole_history_data_cache):
 
215
    def _load_whole_history_data(self, caches, cache_key):
 
216
        """Set the attributes relating to the whole history of the branch.
 
217
 
 
218
        :param caches: a list of caches with interfaces like
 
219
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
 
220
        :param cache_key: the key to use with the caches.
 
221
        """
 
222
        self._rev_indices = None
 
223
        self._rev_info = None
 
224
 
 
225
        missed_caches = []
 
226
        def update_missed_caches():
 
227
            for cache in missed_caches:
 
228
                cache.set(cache_key, self.last_revid, self._rev_info)
 
229
        for cache in caches:
 
230
            data = cache.get(cache_key, self.last_revid)
 
231
            if data is not None:
 
232
                self._rev_info = data
 
233
                update_missed_caches()
 
234
                break
 
235
            else:
 
236
                missed_caches.append(cache)
 
237
        else:
 
238
            whole_history_data = compute_whole_history_data(self._branch)
 
239
            self._rev_info, self._rev_indices = whole_history_data
 
240
            update_missed_caches()
 
241
 
 
242
        if self._rev_indices is not None:
 
243
            self._revno_revid = {}
 
244
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
 
245
                self._revno_revid[revno_str] = revid
 
246
        else:
 
247
            self._revno_revid = {}
 
248
            self._rev_indices = {}
 
249
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
 
250
                self._rev_indices[revid] = seq
 
251
                self._revno_revid[revno_str] = revid
 
252
 
 
253
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
 
254
                 revinfo_disk_cache=None, cache_key=None):
145
255
        assert branch.is_locked(), (
146
256
            "Can only construct a History object with a read-locked branch.")
147
 
        self._file_change_cache = None
 
257
        if file_cache is not None:
 
258
            self._file_change_cache = file_cache
 
259
            file_cache.history = self
 
260
        else:
 
261
            self._file_change_cache = None
148
262
        self._branch = branch
149
263
        self._inventory_cache = {}
150
264
        self._branch_nick = self._branch.get_config().get_nickname()
151
 
        self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
 
265
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
152
266
 
153
267
        self.last_revid = branch.last_revision()
154
268
 
155
 
        whole_history_data = whole_history_data_cache.get(self.last_revid)
156
 
        if whole_history_data is None:
157
 
            whole_history_data = compute_whole_history_data(branch)
158
 
            whole_history_data_cache[self.last_revid] = whole_history_data
159
 
 
160
 
        (self._revision_graph, self._full_history, self._revision_info,
161
 
         self._revno_revid, self._merge_sort, self._where_merged,
162
 
         ) = whole_history_data
163
 
 
164
 
    def use_file_cache(self, cache):
165
 
        self._file_change_cache = cache
 
269
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
 
270
        if revinfo_disk_cache:
 
271
            caches.append(revinfo_disk_cache)
 
272
        self._load_whole_history_data(caches, cache_key)
166
273
 
167
274
    @property
168
275
    def has_revisions(self):
172
279
        return self._branch.get_config()
173
280
 
174
281
    def get_revno(self, revid):
175
 
        if revid not in self._revision_info:
 
282
        if revid not in self._rev_indices:
176
283
            # ghost parent?
177
284
            return 'unknown'
178
 
        (seq, revid, merge_depth,
179
 
         revno_str, end_of_merge) = self._revision_info[revid]
180
 
        return revno_str
 
285
        seq = self._rev_indices[revid]
 
286
        revno = self._rev_info[seq][0][3]
 
287
        return revno
181
288
 
182
289
    def get_revids_from(self, revid_list, start_revid):
183
290
        """
185
292
        revid in revid_list.
186
293
        """
187
294
        if revid_list is None:
188
 
            revid_list = self._full_history
 
295
            revid_list = [r[0][1] for r in self._rev_info]
189
296
        revid_set = set(revid_list)
190
297
        revid = start_revid
191
298
 
192
299
        def introduced_revisions(revid):
193
300
            r = set([revid])
194
 
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
 
301
            seq = self._rev_indices[revid]
 
302
            md = self._rev_info[seq][0][2]
195
303
            i = seq + 1
196
 
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
197
 
                r.add(self._merge_sort[i][1])
 
304
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
 
305
                r.add(self._rev_info[i][0][1])
198
306
                i += 1
199
307
            return r
200
308
        while 1:
202
310
                return
203
311
            if introduced_revisions(revid) & revid_set:
204
312
                yield revid
205
 
            parents = self._revision_graph[revid]
 
313
            parents = self._rev_info[self._rev_indices[revid]][2]
206
314
            if len(parents) == 0:
207
315
                return
208
316
            revid = parents[0]
210
318
    def get_short_revision_history_by_fileid(self, file_id):
211
319
        # FIXME: would be awesome if we could get, for a folder, the list of
212
320
        # revisions where items within that folder changed.i
213
 
        try:
214
 
            # FIXME: Workaround for bzr versions prior to 1.6b3.
215
 
            # Remove me eventually pretty please  :)
216
 
            w = self._branch.repository.weave_store.get_weave(
217
 
                    file_id, self._branch.repository.get_transaction())
218
 
            w_revids = w.versions()
219
 
            revids = [r for r in self._full_history if r in w_revids]
220
 
        except AttributeError:
221
 
            possible_keys = [(file_id, revid) for revid in self._full_history]
222
 
            get_parent_map = self._branch.repository.texts.get_parent_map
223
 
            # We chunk the requests as this works better with GraphIndex.
224
 
            # See _filter_revisions_touching_file_id in bzrlib/log.py
225
 
            # for more information.
226
 
            revids = []
227
 
            chunk_size = 1000
228
 
            for start in xrange(0, len(possible_keys), chunk_size):
229
 
                next_keys = possible_keys[start:start + chunk_size]
230
 
                revids += [k[1] for k in get_parent_map(next_keys)]
231
 
            del possible_keys, next_keys
 
321
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
 
322
        get_parent_map = self._branch.repository.texts.get_parent_map
 
323
        # We chunk the requests as this works better with GraphIndex.
 
324
        # See _filter_revisions_touching_file_id in bzrlib/log.py
 
325
        # for more information.
 
326
        revids = []
 
327
        chunk_size = 1000
 
328
        for start in xrange(0, len(possible_keys), chunk_size):
 
329
            next_keys = possible_keys[start:start + chunk_size]
 
330
            revids += [k[1] for k in get_parent_map(next_keys)]
 
331
        del possible_keys, next_keys
232
332
        return revids
233
333
 
234
334
    def get_revision_history_since(self, revid_list, date):
419
519
 
420
520
        merge_point = []
421
521
        while True:
422
 
            children = self._where_merged.get(revid, [])
 
522
            children = self._rev_info[self._rev_indices[revid]][1]
423
523
            nexts = []
424
524
            for child in children:
425
 
                child_parents = self._revision_graph[child]
 
525
                child_parents = self._rev_info[self._rev_indices[child]][2]
426
526
                if child_parents[0] == revid:
427
527
                    nexts.append(child)
428
528
                else:
448
548
            revnol = revno.split(".")
449
549
            revnos = ".".join(revnol[:-2])
450
550
            revnolast = int(revnol[-1])
451
 
            if revnos in d.keys():
 
551
            if revnos in d:
452
552
                m = d[revnos][0]
453
553
                if revnolast < m:
454
554
                    d[revnos] = (revnolast, revid)
455
555
            else:
456
556
                d[revnos] = (revnolast, revid)
457
557
 
458
 
        return [d[revnos][1] for revnos in d.keys()]
 
558
        return [revid for (_, revid) in d.itervalues()]
459
559
 
460
 
    def get_branch_nicks(self, changes):
 
560
    def add_branch_nicks(self, change):
461
561
        """
462
 
        given a list of changes from L{get_changes}, fill in the branch nicks
463
 
        on all parents and merge points.
 
562
        given a 'change', fill in the branch nicks on all parents and merge
 
563
        points.
464
564
        """
465
565
        fetch_set = set()
466
 
        for change in changes:
467
 
            for p in change.parents:
468
 
                fetch_set.add(p.revid)
469
 
            for p in change.merge_points:
470
 
                fetch_set.add(p.revid)
 
566
        for p in change.parents:
 
567
            fetch_set.add(p.revid)
 
568
        for p in change.merge_points:
 
569
            fetch_set.add(p.revid)
471
570
        p_changes = self.get_changes(list(fetch_set))
472
571
        p_change_dict = dict([(c.revid, c) for c in p_changes])
473
 
        for change in changes:
474
 
            # arch-converted branches may not have merged branch info :(
475
 
            for p in change.parents:
476
 
                if p.revid in p_change_dict:
477
 
                    p.branch_nick = p_change_dict[p.revid].branch_nick
478
 
                else:
479
 
                    p.branch_nick = '(missing)'
480
 
            for p in change.merge_points:
481
 
                if p.revid in p_change_dict:
482
 
                    p.branch_nick = p_change_dict[p.revid].branch_nick
483
 
                else:
484
 
                    p.branch_nick = '(missing)'
 
572
        for p in change.parents:
 
573
            if p.revid in p_change_dict:
 
574
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
575
            else:
 
576
                p.branch_nick = '(missing)'
 
577
        for p in change.merge_points:
 
578
            if p.revid in p_change_dict:
 
579
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
580
            else:
 
581
                p.branch_nick = '(missing)'
485
582
 
486
583
    def get_changes(self, revid_list):
487
584
        """Return a list of changes objects for the given revids.
526
623
 
527
624
        return [self._change_from_revision(rev) for rev in rev_list]
528
625
 
529
 
    def _get_deltas_for_revisions_with_trees(self, revisions):
530
 
        """Produce a list of revision deltas.
531
 
 
532
 
        Note that the input is a sequence of REVISIONS, not revision_ids.
533
 
        Trees will be held in memory until the generator exits.
534
 
        Each delta is relative to the revision's lefthand predecessor.
535
 
        (This is copied from bzrlib.)
536
 
        """
537
 
        required_trees = set()
538
 
        for revision in revisions:
539
 
            required_trees.add(revision.revid)
540
 
            required_trees.update([p.revid for p in revision.parents[:1]])
541
 
        trees = dict((t.get_revision_id(), t) for
542
 
                     t in self._branch.repository.revision_trees(
543
 
                         required_trees))
544
 
        ret = []
545
 
        for revision in revisions:
546
 
            if not revision.parents:
547
 
                old_tree = self._branch.repository.revision_tree(
548
 
                    bzrlib.revision.NULL_REVISION)
549
 
            else:
550
 
                old_tree = trees[revision.parents[0].revid]
551
 
            tree = trees[revision.revid]
552
 
            ret.append(tree.changes_from(old_tree))
553
 
        return ret
554
 
 
555
626
    def _change_from_revision(self, revision):
556
627
        """
557
628
        Given a bzrlib Revision, return a processed "change" for use in
558
629
        templates.
559
630
        """
560
 
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
561
 
 
562
 
        parents = [util.Container(revid=r,
563
 
                   revno=self.get_revno(r)) for r in revision.parent_ids]
564
 
 
565
631
        message, short_message = clean_message(revision.message)
566
632
 
 
633
        tags = self._branch.tags.get_reverse_tag_dict()
 
634
 
 
635
        revtags = None
 
636
        if tags.has_key(revision.revision_id):
 
637
          revtags = ', '.join(tags[revision.revision_id])
 
638
 
567
639
        entry = {
568
640
            'revid': revision.revision_id,
569
 
            'date': commit_time,
570
 
            'author': revision.get_apparent_author(),
 
641
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
 
642
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
 
643
            'authors': revision.get_apparent_authors(),
571
644
            'branch_nick': revision.properties.get('branch-nick', None),
572
645
            'short_comment': short_message,
573
646
            'comment': revision.message,
574
647
            'comment_clean': [util.html_clean(s) for s in message],
575
648
            'parents': revision.parent_ids,
 
649
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
 
650
            'tags': revtags,
576
651
        }
577
652
        return util.Container(entry)
578
653
 
579
 
    def get_file_changes_uncached(self, entries):
580
 
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
581
 
 
582
 
        return [self.parse_delta(delta) for delta in delta_list]
583
 
 
584
 
    def get_file_changes(self, entries):
 
654
    def get_file_changes_uncached(self, entry):
 
655
        if entry.parents:
 
656
            old_revid = entry.parents[0].revid
 
657
        else:
 
658
            old_revid = bzrlib.revision.NULL_REVISION
 
659
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
 
660
 
 
661
    def get_file_changes(self, entry):
585
662
        if self._file_change_cache is None:
586
 
            return self.get_file_changes_uncached(entries)
 
663
            return self.get_file_changes_uncached(entry)
587
664
        else:
588
 
            return self._file_change_cache.get_file_changes(entries)
589
 
 
590
 
    def add_changes(self, entries):
591
 
        changes_list = self.get_file_changes(entries)
592
 
 
593
 
        for entry, changes in zip(entries, changes_list):
594
 
            entry.changes = changes
 
665
            return self._file_change_cache.get_file_changes(entry)
 
666
 
 
667
    def add_changes(self, entry):
 
668
        changes = self.get_file_changes(entry)
 
669
        entry.changes = changes
595
670
 
596
671
    def get_file(self, file_id, revid):
597
672
        "returns (path, filename, data)"
603
678
            path = '/' + path
604
679
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
605
680
 
606
 
    def parse_delta(self, delta):
 
681
    def file_changes_for_revision_ids(self, old_revid, new_revid):
607
682
        """
608
683
        Return a nested data structure containing the changes in a delta::
609
684
 
613
688
            modified: list(
614
689
                filename: str,
615
690
                file_id: str,
616
 
            )
 
691
            ),
 
692
            text_changes: list((filename, file_id)),
617
693
        """
618
 
        added = []
619
 
        modified = []
620
 
        renamed = []
621
 
        removed = []
622
 
 
623
 
        for path, fid, kind in delta.added:
624
 
            added.append((rich_filename(path, kind), fid))
625
 
 
626
 
        for path, fid, kind, text_modified, meta_modified in delta.modified:
627
 
            modified.append(util.Container(filename=rich_filename(path, kind),
628
 
                                           file_id=fid))
629
 
 
630
 
        for old_path, new_path, fid, kind, text_modified, meta_modified in \
631
 
delta.renamed:
632
 
            renamed.append((rich_filename(old_path, kind),
633
 
                            rich_filename(new_path, kind), fid))
634
 
            if meta_modified or text_modified:
635
 
                modified.append(util.Container(
636
 
                    filename=rich_filename(new_path, kind), file_id=fid))
637
 
 
638
 
        for path, fid, kind in delta.removed:
639
 
            removed.append((rich_filename(path, kind), fid))
640
 
 
641
 
        return util.Container(added=added, renamed=renamed,
642
 
                              removed=removed, modified=modified)
643
 
 
644
 
    def annotate_file(self, file_id, revid):
645
 
        z = time.time()
646
 
        lineno = 1
647
 
        parity = 0
648
 
 
649
 
        file_revid = self.get_inventory(revid)[file_id].revision
650
 
        oldvalues = None
651
 
        tree = self._branch.repository.revision_tree(file_revid)
652
 
        revid_set = set()
653
 
 
654
 
        try:
655
 
            bzrlib.textfile.check_text_lines(tree.get_file_lines(file_id))
656
 
        except bzrlib.errors.BinaryFile:
657
 
                # bail out; this isn't displayable text
658
 
                yield util.Container(parity=0, lineno=1, status='same',
659
 
                                     text='(This is a binary file.)',
660
 
                                     change=util.Container())
 
694
        repo = self._branch.repository
 
695
        if (bzrlib.revision.is_null(old_revid) or
 
696
            bzrlib.revision.is_null(new_revid)):
 
697
            old_tree, new_tree = map(
 
698
                repo.revision_tree, [old_revid, new_revid])
661
699
        else:
662
 
            for line_revid, text in tree.annotate_iter(file_id):
663
 
                revid_set.add(line_revid)
664
 
 
665
 
            change_cache = dict([(c.revid, c) \
666
 
                    for c in self.get_changes(list(revid_set))])
667
 
 
668
 
            last_line_revid = None
669
 
            for line_revid, text in tree.annotate_iter(file_id):
670
 
                if line_revid == last_line_revid:
671
 
                    # remember which lines have a new revno and which don't
672
 
                    status = 'same'
673
 
                else:
674
 
                    status = 'changed'
675
 
                    parity ^= 1
676
 
                    last_line_revid = line_revid
677
 
                    change = change_cache[line_revid]
678
 
                    trunc_revno = change.revno
679
 
                    if len(trunc_revno) > 10:
680
 
                        trunc_revno = trunc_revno[:9] + '...'
681
 
 
682
 
                yield util.Container(parity=parity, lineno=lineno, status=status,
683
 
                                     change=change, text=util.fixed_width(text))
684
 
                lineno += 1
685
 
 
686
 
        self.log.debug('annotate: %r secs' % (time.time() - z))
 
700
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
 
701
 
 
702
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
 
703
 
 
704
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
 
705
 
 
706
        return util.Container(
 
707
            added=sorted(reporter.added, key=lambda x:x.filename),
 
708
            renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
 
709
            removed=sorted(reporter.removed, key=lambda x:x.filename),
 
710
            modified=sorted(reporter.modified, key=lambda x:x.filename),
 
711
            text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))