~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:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
34
32
import re
35
33
import textwrap
36
34
import threading
 
35
import time
 
36
from StringIO import StringIO
37
37
 
38
 
from loggerhead import search
39
38
from loggerhead import util
40
 
from loggerhead.wholehistory import compute_whole_history_data
 
39
from loggerhead.util import decorator
41
40
 
42
41
import bzrlib
43
42
import bzrlib.branch
44
 
import bzrlib.delta
 
43
import bzrlib.bundle.serializer
45
44
import bzrlib.diff
46
45
import bzrlib.errors
47
 
import bzrlib.lru_cache
48
46
import bzrlib.progress
49
47
import bzrlib.revision
50
 
import bzrlib.textfile
51
48
import bzrlib.tsort
52
49
import bzrlib.ui
53
50
 
 
51
 
 
52
with_branch_lock = util.with_lock('_lock', 'branch')
 
53
 
 
54
 
 
55
@decorator
 
56
def with_bzrlib_read_lock(unbound):
 
57
    def bzrlib_read_locked(self, *args, **kw):
 
58
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
 
59
        self._branch.repository.lock_read()
 
60
        try:
 
61
            return unbound(self, *args, **kw)
 
62
        finally:
 
63
            self._branch.repository.unlock()
 
64
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
 
65
    return bzrlib_read_locked
 
66
 
 
67
 
54
68
# bzrlib's UIFactory is not thread-safe
55
69
uihack = threading.local()
56
70
 
57
 
 
58
71
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
59
 
 
60
72
    def nested_progress_bar(self):
61
73
        if getattr(uihack, '_progress_bar_stack', None) is None:
62
 
            pbs = bzrlib.progress.ProgressBarStack(
63
 
                      klass=bzrlib.progress.DummyProgress)
64
 
            uihack._progress_bar_stack = pbs
 
74
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
65
75
        return uihack._progress_bar_stack.get_nested()
66
76
 
67
77
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
68
78
 
 
79
 
 
80
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
 
81
    while len(delete_list) < len(insert_list):
 
82
        delete_list.append((None, '', 'context'))
 
83
    while len(insert_list) < len(delete_list):
 
84
        insert_list.append((None, '', 'context'))
 
85
    while len(delete_list) > 0:
 
86
        d = delete_list.pop(0)
 
87
        i = insert_list.pop(0)
 
88
        line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
 
89
                                        old_line=d[1], new_line=i[1],
 
90
                                        old_type=d[2], new_type=i[2]))
 
91
 
 
92
 
 
93
def _make_side_by_side(chunk_list):
 
94
    """
 
95
    turn a normal unified-style diff (post-processed by parse_delta) into a
 
96
    side-by-side diff structure.  the new structure is::
 
97
 
 
98
        chunks: list(
 
99
            diff: list(
 
100
                old_lineno: int,
 
101
                new_lineno: int,
 
102
                old_line: str,
 
103
                new_line: str,
 
104
                type: str('context' or 'changed'),
 
105
            )
 
106
        )
 
107
    """
 
108
    out_chunk_list = []
 
109
    for chunk in chunk_list:
 
110
        line_list = []
 
111
        delete_list, insert_list = [], []
 
112
        for line in chunk.diff:
 
113
            if line.type == 'context':
 
114
                if len(delete_list) or len(insert_list):
 
115
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
116
                    delete_list, insert_list = [], []
 
117
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
 
118
                                                old_line=line.line, new_line=line.line,
 
119
                                                old_type=line.type, new_type=line.type))
 
120
            elif line.type == 'delete':
 
121
                delete_list.append((line.old_lineno, line.line, line.type))
 
122
            elif line.type == 'insert':
 
123
                insert_list.append((line.new_lineno, line.line, line.type))
 
124
        if len(delete_list) or len(insert_list):
 
125
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
126
        out_chunk_list.append(util.Container(diff=line_list))
 
127
    return out_chunk_list
 
128
 
 
129
 
69
130
def is_branch(folder):
70
131
    try:
71
132
        bzrlib.branch.Branch.open(folder)
81
142
    module (Robey, the original author of this code, apparently favored this
82
143
    style of message).
83
144
    """
84
 
    message = message.lstrip().splitlines()
 
145
    message = message.splitlines()
85
146
 
86
147
    if len(message) == 1:
87
148
        message = textwrap.wrap(message[0])
94
155
 
95
156
    # Make short form of commit message.
96
157
    short_message = message[0]
97
 
    if len(short_message) > 60:
98
 
        short_message = short_message[:60] + '...'
 
158
    if len(short_message) > 80:
 
159
        short_message = short_message[:80] + '...'
99
160
 
100
161
    return message, short_message
101
162
 
108
169
    return path
109
170
 
110
171
 
 
172
 
111
173
# from bzrlib
112
 
 
113
 
 
114
174
class _RevListToTimestamps(object):
115
175
    """This takes a list of revisions, and allows you to bisect by date"""
116
176
 
122
182
 
123
183
    def __getitem__(self, index):
124
184
        """Get the date of the index'd item"""
125
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
126
 
                   self.revid_list[index]).timestamp)
 
185
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
127
186
 
128
187
    def __len__(self):
129
188
        return len(self.revid_list)
130
189
 
131
 
class FileChangeReporter(object):
132
 
    def __init__(self, old_inv, new_inv):
133
 
        self.added = []
134
 
        self.modified = []
135
 
        self.renamed = []
136
 
        self.removed = []
137
 
        self.text_changes = []
138
 
        self.old_inv = old_inv
139
 
        self.new_inv = new_inv
140
 
 
141
 
    def revid(self, inv, file_id):
142
 
        try:
143
 
            return inv[file_id].revision
144
 
        except bzrlib.errors.NoSuchId:
145
 
            return 'null:'
146
 
 
147
 
    def report(self, file_id, paths, versioned, renamed, modified,
148
 
               exe_change, kind):
149
 
        if modified not in ('unchanged', 'kind changed'):
150
 
            if versioned == 'removed':
151
 
                filename = rich_filename(paths[0], kind[0])
152
 
            else:
153
 
                filename = rich_filename(paths[1], kind[1])
154
 
            self.text_changes.append(util.Container(
155
 
                filename=filename, file_id=file_id,
156
 
                old_revision=self.revid(self.old_inv, file_id),
157
 
                new_revision=self.revid(self.new_inv, file_id)))
158
 
        if versioned == 'added':
159
 
            self.added.append(util.Container(
160
 
                filename=rich_filename(paths[1], kind),
161
 
                file_id=file_id, kind=kind[1]))
162
 
        elif versioned == 'removed':
163
 
            self.removed.append(util.Container(
164
 
                filename=rich_filename(paths[0], kind),
165
 
                file_id=file_id, kind=kind[0]))
166
 
        elif renamed:
167
 
            self.renamed.append(util.Container(
168
 
                old_filename=rich_filename(paths[0], kind[0]),
169
 
                new_filename=rich_filename(paths[1], kind[1]),
170
 
                file_id=file_id,
171
 
                text_modified=modified == 'modified'))
172
 
        else:
173
 
            self.modified.append(util.Container(
174
 
                filename=rich_filename(paths[1], kind),
175
 
                file_id=file_id))
176
 
 
177
 
 
178
 
class RevInfoMemoryCache(object):
179
 
    """A store that validates values against the revids they were stored with.
180
 
 
181
 
    We use a unique key for each branch.
182
 
 
183
 
    The reason for not just using the revid as the key is so that when a new
184
 
    value is provided for a branch, we replace the old value used for the
185
 
    branch.
186
 
 
187
 
    There is another implementation of the same interface in
188
 
    loggerhead.changecache.RevInfoDiskCache.
189
 
    """
190
 
 
191
 
    def __init__(self, cache):
192
 
        self._cache = cache
193
 
 
194
 
    def get(self, key, revid):
195
 
        """Return the data associated with `key`, subject to a revid check.
196
 
 
197
 
        If a value was stored under `key`, with the same revid, return it.
198
 
        Otherwise return None.
199
 
        """
200
 
        cached = self._cache.get(key)
201
 
        if cached is None:
202
 
            return None
203
 
        stored_revid, data = cached
204
 
        if revid == stored_revid:
205
 
            return data
206
 
        else:
207
 
            return None
208
 
 
209
 
    def set(self, key, revid, data):
210
 
        """Store `data` under `key`, to be checked against `revid` on get().
211
 
        """
212
 
        self._cache[key] = (revid, data)
213
 
 
214
190
 
215
191
class History (object):
216
 
    """Decorate a branch to provide information for rendering.
217
 
 
218
 
    History objects are expected to be short lived -- when serving a request
219
 
    for a particular branch, open it, read-lock it, wrap a History object
220
 
    around it, serve the request, throw the History object away, unlock the
221
 
    branch and throw it away.
222
 
 
223
 
    :ivar _file_change_cache: An object that caches information about the
224
 
        files that changed between two revisions.
225
 
    :ivar _rev_info: A list of information about revisions.  This is by far
226
 
        the most cryptic data structure in loggerhead.  At the top level, it
227
 
        is a list of 3-tuples [(merge-info, where-merged, parents)].
228
 
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
229
 
        like a merged sorted list, but the revno is stringified.
230
 
        `where-merged` is a tuple of revisions that have this revision as a
231
 
        non-lefthand parent.  Finally, `parents` is just the usual list of
232
 
        parents of this revision.
233
 
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
234
 
        the information about it in _rev_info.
235
 
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
236
 
        ids.
237
 
    """
238
 
 
239
 
    def _load_whole_history_data(self, caches, cache_key):
240
 
        """Set the attributes relating to the whole history of the branch.
241
 
 
242
 
        :param caches: a list of caches with interfaces like
243
 
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
244
 
        :param cache_key: the key to use with the caches.
245
 
        """
246
 
        self._rev_indices = None
247
 
        self._rev_info = None
248
 
 
249
 
        missed_caches = []
250
 
        def update_missed_caches():
251
 
            for cache in missed_caches:
252
 
                cache.set(cache_key, self.last_revid, self._rev_info)
253
 
        for cache in caches:
254
 
            data = cache.get(cache_key, self.last_revid)
255
 
            if data is not None:
256
 
                self._rev_info = data
257
 
                update_missed_caches()
258
 
                break
259
 
            else:
260
 
                missed_caches.append(cache)
261
 
        else:
262
 
            whole_history_data = compute_whole_history_data(self._branch)
263
 
            self._rev_info, self._rev_indices = whole_history_data
264
 
            update_missed_caches()
265
 
 
266
 
        if self._rev_indices is not None:
267
 
            self._revno_revid = {}
268
 
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
269
 
                self._revno_revid[revno_str] = revid
270
 
        else:
271
 
            self._revno_revid = {}
272
 
            self._rev_indices = {}
273
 
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
274
 
                self._rev_indices[revid] = seq
275
 
                self._revno_revid[revno_str] = revid
276
 
 
277
 
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
278
 
                 revinfo_disk_cache=None, cache_key=None):
279
 
        assert branch.is_locked(), (
280
 
            "Can only construct a History object with a read-locked branch.")
281
 
        if file_cache is not None:
282
 
            self._file_change_cache = file_cache
283
 
            file_cache.history = self
284
 
        else:
285
 
            self._file_change_cache = None
 
192
 
 
193
    def __init__(self):
 
194
        self._change_cache = None
 
195
        self._file_change_cache = None
 
196
        self._index = None
 
197
        self._lock = threading.RLock()
 
198
 
 
199
    @classmethod
 
200
    def from_branch(cls, branch, name=None):
 
201
        z = time.time()
 
202
        self = cls()
286
203
        self._branch = branch
287
 
        self._inventory_cache = {}
288
 
        self._branch_nick = self._branch.get_config().get_nickname()
289
 
        self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
290
 
 
291
 
        self.last_revid = branch.last_revision()
292
 
 
293
 
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
294
 
        if revinfo_disk_cache:
295
 
            caches.append(revinfo_disk_cache)
296
 
        self._load_whole_history_data(caches, cache_key)
 
204
        self._last_revid = self._branch.last_revision()
 
205
        self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
 
206
 
 
207
        if name is None:
 
208
            name = self._branch.nick
 
209
        self._name = name
 
210
        self.log = logging.getLogger('loggerhead.%s' % (name,))
 
211
 
 
212
        self._full_history = []
 
213
        self._revision_info = {}
 
214
        self._revno_revid = {}
 
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)
 
220
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
 
221
            self._full_history.append(revid)
 
222
            revno_str = '.'.join(str(n) for n in revno)
 
223
            self._revno_revid[revno_str] = revid
 
224
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
 
225
        # cache merge info
 
226
        self._where_merged = {}
 
227
        for revid in self._revision_graph.keys():
 
228
            if self._revision_info[revid][2] == 0:
 
229
                continue
 
230
            for parent in self._revision_graph[revid]:
 
231
                self._where_merged.setdefault(parent, set()).add(revid)
 
232
 
 
233
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
 
234
        return self
 
235
 
 
236
    @classmethod
 
237
    def from_folder(cls, path, name=None):
 
238
        b = bzrlib.branch.Branch.open(path)
 
239
        b.lock_read()
 
240
        try:
 
241
            return cls.from_branch(b, name)
 
242
        finally:
 
243
            b.unlock()
 
244
 
 
245
    @with_branch_lock
 
246
    def out_of_date(self):
 
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
 
256
 
 
257
    def use_cache(self, cache):
 
258
        self._change_cache = cache
 
259
 
 
260
    def use_file_cache(self, cache):
 
261
        self._file_change_cache = cache
 
262
 
 
263
    def use_search_index(self, index):
 
264
        self._index = index
297
265
 
298
266
    @property
299
267
    def has_revisions(self):
300
268
        return not bzrlib.revision.is_null(self.last_revid)
301
269
 
 
270
    @with_branch_lock
 
271
    def detach(self):
 
272
        # called when a new history object needs to be created, because the
 
273
        # branch history has changed.  we need to immediately close and stop
 
274
        # using our caches, because a new history object will be created to
 
275
        # replace us, using the same cache files.
 
276
        # (may also be called during server shutdown.)
 
277
        if self._change_cache is not None:
 
278
            self._change_cache.close()
 
279
            self._change_cache = None
 
280
        if self._index is not None:
 
281
            self._index.close()
 
282
            self._index = None
 
283
 
 
284
    def flush_cache(self):
 
285
        if self._change_cache is None:
 
286
            return
 
287
        self._change_cache.flush()
 
288
 
 
289
    def check_rebuild(self):
 
290
        if self._change_cache is not None:
 
291
            self._change_cache.check_rebuild()
 
292
        if self._index is not None:
 
293
            self._index.check_rebuild()
 
294
 
 
295
    last_revid = property(lambda self: self._last_revid, None, None)
 
296
 
 
297
    @with_branch_lock
302
298
    def get_config(self):
303
299
        return self._branch.get_config()
304
300
 
305
301
    def get_revno(self, revid):
306
 
        if revid not in self._rev_indices:
 
302
        if revid not in self._revision_info:
307
303
            # ghost parent?
308
304
            return 'unknown'
309
 
        seq = self._rev_indices[revid]
310
 
        revno = self._rev_info[seq][0][3]
311
 
        return revno
312
 
 
313
 
    def get_revids_from(self, revid_list, start_revid):
314
 
        """
315
 
        Yield the mainline (wrt start_revid) revisions that merged each
316
 
        revid in revid_list.
317
 
        """
318
 
        if revid_list is None:
319
 
            revid_list = [r[0][1] for r in self._rev_info]
320
 
        revid_set = set(revid_list)
321
 
        revid = start_revid
322
 
 
323
 
        def introduced_revisions(revid):
324
 
            r = set([revid])
325
 
            seq = self._rev_indices[revid]
326
 
            md = self._rev_info[seq][0][2]
327
 
            i = seq + 1
328
 
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
329
 
                r.add(self._rev_info[i][0][1])
330
 
                i += 1
331
 
            return r
332
 
        while 1:
333
 
            if bzrlib.revision.is_null(revid):
334
 
                return
335
 
            if introduced_revisions(revid) & revid_set:
 
305
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
306
        return revno_str
 
307
 
 
308
    def get_revision_history(self):
 
309
        return self._full_history
 
310
 
 
311
    def get_revids_from(self, revid_list, revid):
 
312
        """
 
313
        given a list of revision ids, yield revisions in graph order,
 
314
        starting from revid.  the list can be None if you just want to travel
 
315
        across all revisions.
 
316
        """
 
317
        while True:
 
318
            if (revid_list is None) or (revid in revid_list):
336
319
                yield revid
337
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
320
            if not self._revision_graph.has_key(revid):
 
321
                return
 
322
            parents = self._revision_graph[revid]
338
323
            if len(parents) == 0:
339
324
                return
340
325
            revid = parents[0]
341
326
 
 
327
    @with_branch_lock
342
328
    def get_short_revision_history_by_fileid(self, file_id):
 
329
        # wow.  is this really the only way we can get this list?  by
 
330
        # man-handling the weave store directly? :-0
343
331
        # FIXME: would be awesome if we could get, for a folder, the list of
344
 
        # revisions where items within that folder changed.i
345
 
        try:
346
 
            # FIXME: Workaround for bzr versions prior to 1.6b3.
347
 
            # Remove me eventually pretty please  :)
348
 
            w = self._branch.repository.weave_store.get_weave(
349
 
                    file_id, self._branch.repository.get_transaction())
350
 
            w_revids = w.versions()
351
 
            revids = [r for r in self._rev_indices if r in w_revids]
352
 
        except AttributeError:
353
 
            possible_keys = [(file_id, revid) for revid in self._rev_indices]
354
 
            get_parent_map = self._branch.repository.texts.get_parent_map
355
 
            # We chunk the requests as this works better with GraphIndex.
356
 
            # See _filter_revisions_touching_file_id in bzrlib/log.py
357
 
            # for more information.
358
 
            revids = []
359
 
            chunk_size = 1000
360
 
            for start in xrange(0, len(possible_keys), chunk_size):
361
 
                next_keys = possible_keys[start:start + chunk_size]
362
 
                revids += [k[1] for k in get_parent_map(next_keys)]
363
 
            del possible_keys, next_keys
 
332
        # revisions where items within that folder changed.
 
333
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
334
        w_revids = w.versions()
 
335
        revids = [r for r in self._full_history if r in w_revids]
364
336
        return revids
365
337
 
 
338
    @with_branch_lock
366
339
    def get_revision_history_since(self, revid_list, date):
367
340
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
368
341
        # so start at midnight on 02-sep.
369
342
        date = date + datetime.timedelta(days=1)
370
 
        # our revid list is sorted in REVERSE date order,
371
 
        # so go thru some hoops here...
 
343
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
372
344
        revid_list.reverse()
373
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
374
 
                                                   self._branch.repository),
375
 
                              date)
 
345
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
376
346
        if index == 0:
377
347
            return []
378
348
        revid_list.reverse()
379
349
        index = -index
380
350
        return revid_list[index:]
381
351
 
 
352
    @with_branch_lock
 
353
    def get_revision_history_matching(self, revid_list, text):
 
354
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
355
        z = time.time()
 
356
        # this is going to be painfully slow. :(
 
357
        out = []
 
358
        text = text.lower()
 
359
        for revid in revid_list:
 
360
            change = self.get_changes([ revid ])[0]
 
361
            if text in change.comment.lower():
 
362
                out.append(revid)
 
363
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
 
364
        return out
 
365
 
 
366
    def get_revision_history_matching_indexed(self, revid_list, text):
 
367
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
368
        z = time.time()
 
369
        if self._index is None:
 
370
            return self.get_revision_history_matching(revid_list, text)
 
371
        out = self._index.find(text, revid_list)
 
372
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
 
373
        # put them in some coherent order :)
 
374
        out = [r for r in self._full_history if r in out]
 
375
        return out
 
376
 
 
377
    @with_branch_lock
382
378
    def get_search_revid_list(self, query, revid_list):
383
379
        """
384
380
        given a "quick-search" query, try a few obvious possible meanings:
385
381
 
386
382
            - revision id or # ("128.1.3")
387
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
388
 
iso style "yyyy-mm-dd")
 
383
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
389
384
            - comment text as a fallback
390
385
 
391
386
        and return a revid list that matches.
394
389
        # all the relevant changes (time-consuming) only to return a list of
395
390
        # revids which will be used to fetch a set of changes again.
396
391
 
397
 
        # if they entered a revid, just jump straight there;
398
 
        # ignore the passed-in revid_list
 
392
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
399
393
        revid = self.fix_revid(query)
400
394
        if revid is not None:
401
395
            if isinstance(revid, unicode):
402
396
                revid = revid.encode('utf-8')
403
 
            changes = self.get_changes([revid])
 
397
            changes = self.get_changes([ revid ])
404
398
            if (changes is not None) and (len(changes) > 0):
405
 
                return [revid]
 
399
                return [ revid ]
406
400
 
407
401
        date = None
408
402
        m = self.us_date_re.match(query)
409
403
        if m is not None:
410
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
411
 
                                     int(m.group(1)),
412
 
                                     int(m.group(2)))
 
404
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
413
405
        else:
414
406
            m = self.earth_date_re.match(query)
415
407
            if m is not None:
416
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
417
 
                                         int(m.group(2)),
418
 
                                         int(m.group(1)))
 
408
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
419
409
            else:
420
410
                m = self.iso_date_re.match(query)
421
411
                if m is not None:
422
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
423
 
                                             int(m.group(2)),
424
 
                                             int(m.group(3)))
 
412
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
425
413
        if date is not None:
426
414
            if revid_list is None:
427
 
                # if no limit to the query was given,
428
 
                # search only the direct-parent path.
429
 
                revid_list = list(self.get_revids_from(None, self.last_revid))
 
415
                # if no limit to the query was given, search only the direct-parent path.
 
416
                revid_list = list(self.get_revids_from(None, self._last_revid))
430
417
            return self.get_revision_history_since(revid_list, date)
431
418
 
 
419
        # check comment fields.
 
420
        if revid_list is None:
 
421
            revid_list = self._full_history
 
422
        return self.get_revision_history_matching_indexed(revid_list, query)
 
423
 
432
424
    revno_re = re.compile(r'^[\d\.]+$')
433
425
    # the date regex are without a final '$' so that queries like
434
426
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
442
434
        if revid is None:
443
435
            return revid
444
436
        if revid == 'head:':
445
 
            return self.last_revid
446
 
        try:
447
 
            if self.revno_re.match(revid):
448
 
                revid = self._revno_revid[revid]
449
 
        except KeyError:
450
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
437
            return self._last_revid
 
438
        if self.revno_re.match(revid):
 
439
            revid = self._revno_revid[revid]
451
440
        return revid
452
441
 
 
442
    @with_branch_lock
453
443
    def get_file_view(self, revid, file_id):
454
444
        """
455
445
        Given a revid and optional path, return a (revlist, revid) for
459
449
        If file_id is None, the entire revision history is the list scope.
460
450
        """
461
451
        if revid is None:
462
 
            revid = self.last_revid
 
452
            revid = self._last_revid
463
453
        if file_id is not None:
464
454
            # since revid is 'start_revid', possibly should start the path
465
455
            # tracing from revid... FIXME
469
459
            revlist = list(self.get_revids_from(None, revid))
470
460
        return revlist
471
461
 
 
462
    @with_branch_lock
472
463
    def get_view(self, revid, start_revid, file_id, query=None):
473
464
        """
474
465
        use the URL parameters (revid, start_revid, file_id, and query) to
494
485
        contain vital context for future url navigation.
495
486
        """
496
487
        if start_revid is None:
497
 
            start_revid = self.last_revid
 
488
            start_revid = self._last_revid
498
489
 
499
490
        if query is None:
500
491
            revid_list = self.get_file_view(start_revid, file_id)
503
494
            if revid not in revid_list:
504
495
                # if the given revid is not in the revlist, use a revlist that
505
496
                # starts at the given revid.
506
 
                revid_list = self.get_file_view(revid, file_id)
 
497
                revid_list= self.get_file_view(revid, file_id)
507
498
                start_revid = revid
508
499
            return revid, start_revid, revid_list
509
500
 
512
503
            revid_list = self.get_file_view(start_revid, file_id)
513
504
        else:
514
505
            revid_list = None
515
 
        revid_list = search.search_revisions(self._branch, query)
516
 
        if revid_list and len(revid_list) > 0:
 
506
 
 
507
        revid_list = self.get_search_revid_list(query, revid_list)
 
508
        if len(revid_list) > 0:
517
509
            if revid not in revid_list:
518
510
                revid = revid_list[0]
519
511
            return revid, start_revid, revid_list
520
512
        else:
521
 
            # XXX: This should return a message saying that the search could
522
 
            # not be completed due to either missing the plugin or missing a
523
 
            # search index.
 
513
            # no results
524
514
            return None, None, []
525
515
 
 
516
    @with_branch_lock
526
517
    def get_inventory(self, revid):
527
 
        if revid not in self._inventory_cache:
528
 
            self._inventory_cache[revid] = (
529
 
                self._branch.repository.get_revision_inventory(revid))
530
 
        return self._inventory_cache[revid]
 
518
        return self._branch.repository.get_revision_inventory(revid)
531
519
 
 
520
    @with_branch_lock
532
521
    def get_path(self, revid, file_id):
533
522
        if (file_id is None) or (file_id == ''):
534
523
            return ''
535
 
        path = self.get_inventory(revid).id2path(file_id)
 
524
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
536
525
        if (len(path) > 0) and not path.startswith('/'):
537
526
            path = '/' + path
538
527
        return path
539
528
 
 
529
    @with_branch_lock
540
530
    def get_file_id(self, revid, path):
541
531
        if (len(path) > 0) and not path.startswith('/'):
542
532
            path = '/' + path
543
 
        return self.get_inventory(revid).path2id(path)
 
533
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
 
534
 
544
535
 
545
536
    def get_merge_point_list(self, revid):
546
537
        """
551
542
 
552
543
        merge_point = []
553
544
        while True:
554
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
545
            children = self._where_merged.get(revid, [])
555
546
            nexts = []
556
547
            for child in children:
557
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
548
                child_parents = self._revision_graph[child]
558
549
                if child_parents[0] == revid:
559
550
                    nexts.append(child)
560
551
                else:
580
571
            revnol = revno.split(".")
581
572
            revnos = ".".join(revnol[:-2])
582
573
            revnolast = int(revnol[-1])
583
 
            if revnos in d.keys():
 
574
            if d.has_key(revnos):
584
575
                m = d[revnos][0]
585
576
                if revnolast < m:
586
 
                    d[revnos] = (revnolast, revid)
 
577
                    d[revnos] = ( revnolast, revid )
587
578
            else:
588
 
                d[revnos] = (revnolast, revid)
589
 
 
590
 
        return [d[revnos][1] for revnos in d.keys()]
591
 
 
592
 
    def add_branch_nicks(self, change):
 
579
                d[revnos] = ( revnolast, revid )
 
580
 
 
581
        return [ d[revnos][1] for revnos in d.keys() ]
 
582
 
 
583
    def get_branch_nicks(self, changes):
593
584
        """
594
 
        given a 'change', fill in the branch nicks on all parents and merge
595
 
        points.
 
585
        given a list of changes from L{get_changes}, fill in the branch nicks
 
586
        on all parents and merge points.
596
587
        """
597
588
        fetch_set = set()
598
 
        for p in change.parents:
599
 
            fetch_set.add(p.revid)
600
 
        for p in change.merge_points:
601
 
            fetch_set.add(p.revid)
 
589
        for change in changes:
 
590
            for p in change.parents:
 
591
                fetch_set.add(p.revid)
 
592
            for p in change.merge_points:
 
593
                fetch_set.add(p.revid)
602
594
        p_changes = self.get_changes(list(fetch_set))
603
595
        p_change_dict = dict([(c.revid, c) for c in p_changes])
604
 
        for p in change.parents:
605
 
            if p.revid in p_change_dict:
606
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
607
 
            else:
608
 
                p.branch_nick = '(missing)'
609
 
        for p in change.merge_points:
610
 
            if p.revid in p_change_dict:
611
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
612
 
            else:
613
 
                p.branch_nick = '(missing)'
 
596
        for change in changes:
 
597
            # arch-converted branches may not have merged branch info :(
 
598
            for p in change.parents:
 
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)'
 
603
            for p in change.merge_points:
 
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)'
614
608
 
 
609
    @with_branch_lock
615
610
    def get_changes(self, revid_list):
616
611
        """Return a list of changes objects for the given revids.
617
612
 
618
613
        Revisions not present and NULL_REVISION will be ignored.
619
614
        """
620
 
        changes = self.get_changes_uncached(revid_list)
 
615
        if self._change_cache is None:
 
616
            changes = self.get_changes_uncached(revid_list)
 
617
        else:
 
618
            changes = self._change_cache.get_changes(revid_list)
621
619
        if len(changes) == 0:
622
620
            return changes
623
621
 
624
622
        # some data needs to be recalculated each time, because it may
625
623
        # change as new revisions are added.
626
624
        for change in changes:
627
 
            merge_revids = self.simplify_merge_point_list(
628
 
                               self.get_merge_point_list(change.revid))
629
 
            change.merge_points = [
630
 
                util.Container(revid=r,
631
 
                revno=self.get_revno(r)) for r in merge_revids]
 
625
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
 
626
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
632
627
            if len(change.parents) > 0:
633
 
                change.parents = [util.Container(revid=r,
634
 
                    revno=self.get_revno(r)) for r in change.parents]
 
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]
635
633
            change.revno = self.get_revno(change.revid)
636
634
 
637
635
        parity = 0
641
639
 
642
640
        return changes
643
641
 
 
642
    # alright, let's profile this sucka. (FIXME remove this eventually...)
 
643
    def _get_changes_profiled(self, revid_list):
 
644
        from loggerhead.lsprof import profile
 
645
        import cPickle
 
646
        ret, stats = profile(self.get_changes_uncached, revid_list)
 
647
        stats.sort()
 
648
        stats.freeze()
 
649
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
 
650
        self.log.info('lsprof complete!')
 
651
        return ret
 
652
 
 
653
    @with_branch_lock
 
654
    @with_bzrlib_read_lock
644
655
    def get_changes_uncached(self, revid_list):
645
 
        # FIXME: deprecated method in getting a null revision
646
656
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
647
657
                            revid_list)
648
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
649
 
                         revid_list)
 
658
        repo = self._branch.repository
 
659
        parent_map = repo.get_graph().get_parent_map(revid_list)
650
660
        # We need to return the answer in the same order as the input,
651
661
        # less any ghosts.
652
662
        present_revids = [revid for revid in revid_list
653
663
                          if revid in parent_map]
654
 
        rev_list = self._branch.repository.get_revisions(present_revids)
 
664
        rev_list = repo.get_revisions(present_revids)
655
665
 
656
666
        return [self._change_from_revision(rev) for rev in rev_list]
657
667
 
 
668
    def _get_deltas_for_revisions_with_trees(self, revisions):
 
669
        """Produce a list of revision deltas.
 
670
 
 
671
        Note that the input is a sequence of REVISIONS, not revision_ids.
 
672
        Trees will be held in memory until the generator exits.
 
673
        Each delta is relative to the revision's lefthand predecessor.
 
674
        (This is copied from bzrlib.)
 
675
        """
 
676
        required_trees = set()
 
677
        for revision in revisions:
 
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
 
681
                     t in self._branch.repository.revision_trees(required_trees))
 
682
        ret = []
 
683
        self._branch.repository.lock_read()
 
684
        try:
 
685
            for revision in revisions:
 
686
                if not revision.parents:
 
687
                    old_tree = self._branch.repository.revision_tree(
 
688
                        bzrlib.revision.NULL_REVISION)
 
689
                else:
 
690
                    old_tree = trees[revision.parents[0].revid]
 
691
                tree = trees[revision.revid]
 
692
                ret.append(tree.changes_from(old_tree))
 
693
            return ret
 
694
        finally:
 
695
            self._branch.repository.unlock()
 
696
 
658
697
    def _change_from_revision(self, revision):
659
698
        """
660
699
        Given a bzrlib Revision, return a processed "change" for use in
662
701
        """
663
702
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
664
703
 
665
 
        parents = [util.Container(revid=r,
666
 
                   revno=self.get_revno(r)) for r in revision.parent_ids]
 
704
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
667
705
 
668
706
        message, short_message = clean_message(revision.message)
669
707
 
670
 
        try:
671
 
            authors = revision.get_apparent_authors()
672
 
        except AttributeError:
673
 
            authors = [revision.get_apparent_author()]
674
 
 
675
708
        entry = {
676
709
            'revid': revision.revision_id,
677
710
            'date': commit_time,
678
 
            'authors': authors,
 
711
            'author': revision.committer,
679
712
            'branch_nick': revision.properties.get('branch-nick', None),
680
713
            'short_comment': short_message,
681
714
            'comment': revision.message,
684
717
        }
685
718
        return util.Container(entry)
686
719
 
687
 
    def get_file_changes_uncached(self, entry):
688
 
        repo = self._branch.repository
689
 
        if entry.parents:
690
 
            old_revid = entry.parents[0].revid
691
 
        else:
692
 
            old_revid = bzrlib.revision.NULL_REVISION
693
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
694
 
 
695
 
    def get_file_changes(self, entry):
 
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):
696
727
        if self._file_change_cache is None:
697
 
            return self.get_file_changes_uncached(entry)
 
728
            return self.get_file_changes_uncached(entries)
698
729
        else:
699
 
            return self._file_change_cache.get_file_changes(entry)
700
 
 
701
 
    def add_changes(self, entry):
702
 
        changes = self.get_file_changes(entry)
703
 
        entry.changes = changes
704
 
 
 
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)
 
750
        delta = rev_tree2.changes_from(rev_tree1)
 
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
 
 
757
    @with_branch_lock
705
758
    def get_file(self, file_id, revid):
706
759
        "returns (path, filename, data)"
707
760
        inv = self.get_inventory(revid)
712
765
            path = '/' + path
713
766
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
714
767
 
715
 
    def file_changes_for_revision_ids(self, old_revid, new_revid):
 
768
    def _parse_diffs(self, old_tree, new_tree, delta):
 
769
        """
 
770
        Return a list of processed diffs, in the format::
 
771
 
 
772
            list(
 
773
                filename: str,
 
774
                file_id: str,
 
775
                chunks: list(
 
776
                    diff: list(
 
777
                        old_lineno: int,
 
778
                        new_lineno: int,
 
779
                        type: str('context', 'delete', or 'insert'),
 
780
                        line: str,
 
781
                    ),
 
782
                ),
 
783
            )
 
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):
716
851
        """
717
852
        Return a nested data structure containing the changes in a delta::
718
853
 
722
857
            modified: list(
723
858
                filename: str,
724
859
                file_id: str,
725
 
            ),
726
 
            text_changes: list((filename, file_id)),
727
 
        """
728
 
        repo = self._branch.repository
729
 
        if bzrlib.revision.is_null(old_revid) or \
730
 
               bzrlib.revision.is_null(new_revid):
731
 
            old_tree, new_tree = map(
732
 
                repo.revision_tree, [old_revid, new_revid])
733
 
        else:
734
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
735
 
 
736
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
737
 
 
738
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
739
 
 
740
 
        return util.Container(
741
 
            added=sorted(reporter.added, key=lambda x:x.filename),
742
 
            renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
743
 
            removed=sorted(reporter.removed, key=lambda x:x.filename),
744
 
            modified=sorted(reporter.modified, key=lambda x:x.filename),
745
 
            text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))
 
860
            )
 
861
        """
 
862
        added = []
 
863
        modified = []
 
864
        renamed = []
 
865
        removed = []
 
866
 
 
867
        for path, fid, kind in delta.added:
 
868
            added.append((rich_filename(path, kind), fid))
 
869
 
 
870
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
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))
 
875
            if meta_modified or text_modified:
 
876
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
877
 
 
878
        for path, fid, kind in delta.removed:
 
879
            removed.append((rich_filename(path, kind), fid))
 
880
 
 
881
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
 
882
 
 
883
    @staticmethod
 
884
    def add_side_by_side(changes):
 
885
        # FIXME: this is a rotten API.
 
886
        for change in changes:
 
887
            for m in change.changes.modified:
 
888
                m.sbs_chunks = _make_side_by_side(m.chunks)
 
889
 
 
890
    @with_branch_lock
 
891
    def get_filelist(self, inv, file_id, sort_type=None):
 
892
        """
 
893
        return the list of all files (and their attributes) within a given
 
894
        path subtree.
 
895
        """
 
896
 
 
897
        dir_ie = inv[file_id]
 
898
        path = inv.id2path(file_id)
 
899
        file_list = []
 
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():
 
911
            pathname = filename
 
912
            if entry.kind == 'directory':
 
913
                pathname += '/'
 
914
 
 
915
            revid = entry.revision
 
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])
 
921
            file_list.append(file)
 
922
 
 
923
        if sort_type == 'filename' or sort_type is None:
 
924
            file_list.sort(key=lambda x: x.filename)
 
925
        elif sort_type == 'size':
 
926
            file_list.sort(key=lambda x: x.size)
 
927
        elif sort_type == 'date':
 
928
            file_list.sort(key=lambda x: x.change.date)
 
929
 
 
930
        parity = 0
 
931
        for file in file_list:
 
932
            file.parity = parity
 
933
            parity ^= 1
 
934
 
 
935
        return file_list
 
936
 
 
937
 
 
938
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
 
939
 
 
940
    @with_branch_lock
 
941
    def annotate_file(self, file_id, revid):
 
942
        z = time.time()
 
943
        lineno = 1
 
944
        parity = 0
 
945
 
 
946
        file_revid = self.get_inventory(revid)[file_id].revision
 
947
        oldvalues = None
 
948
 
 
949
        # because we cache revision metadata ourselves, it's actually much
 
950
        # faster to call 'annotate_iter' on the weave directly than it is to
 
951
        # ask bzrlib to annotate for us.
 
952
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
953
 
 
954
        revid_set = set()
 
955
        for line_revid, text in w.annotate_iter(file_revid):
 
956
            revid_set.add(line_revid)
 
957
            if self._BADCHARS_RE.match(text):
 
958
                # bail out; this isn't displayable text
 
959
                yield util.Container(parity=0, lineno=1, status='same',
 
960
                                     text='(This is a binary file.)',
 
961
                                     change=util.Container())
 
962
                return
 
963
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
 
964
 
 
965
        last_line_revid = None
 
966
        for line_revid, text in w.annotate_iter(file_revid):
 
967
            if line_revid == last_line_revid:
 
968
                # remember which lines have a new revno and which don't
 
969
                status = 'same'
 
970
            else:
 
971
                status = 'changed'
 
972
                parity ^= 1
 
973
                last_line_revid = line_revid
 
974
                change = change_cache[line_revid]
 
975
                trunc_revno = change.revno
 
976
                if len(trunc_revno) > 10:
 
977
                    trunc_revno = trunc_revno[:9] + '...'
 
978
 
 
979
            yield util.Container(parity=parity, lineno=lineno, status=status,
 
980
                                 change=change, text=util.fixed_width(text))
 
981
            lineno += 1
 
982
 
 
983
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
984
 
 
985
    @with_branch_lock
 
986
    def get_bundle(self, revid, compare_revid=None):
 
987
        if compare_revid is None:
 
988
            parents = self._revision_graph[revid]
 
989
            if len(parents) > 0:
 
990
                compare_revid = parents[0]
 
991
            else:
 
992
                compare_revid = None
 
993
        s = StringIO()
 
994
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
 
995
        return s.getvalue()