~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2007-10-29 16:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 141.
  • Revision ID: michael.hudson@canonical.com-20071029161930-oxqrd4rd8j1oz3hx
add do nothing check target

Show diffs side-by-side

added added

removed removed

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