~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

by accident, i noticed today that loggerhead is sometimes leaving a lot of
file descriptors open.  i think they all come from this one piece of code
that could potentially return without closing the changes cache.  grr.

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