~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  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
 
49
import bzrlib.bundle.serializer
 
50
import bzrlib.decorators
47
51
import bzrlib.diff
48
52
import bzrlib.errors
49
53
import bzrlib.progress
52
56
import bzrlib.tsort
53
57
import bzrlib.ui
54
58
 
 
59
 
 
60
with_branch_lock = util.with_lock('_lock', 'branch')
 
61
 
 
62
 
55
63
# bzrlib's UIFactory is not thread-safe
56
64
uihack = threading.local()
57
65
 
58
 
 
59
66
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
60
 
 
61
67
    def nested_progress_bar(self):
62
68
        if getattr(uihack, '_progress_bar_stack', None) is None:
63
 
            pbs = bzrlib.progress.ProgressBarStack(
64
 
                      klass=bzrlib.progress.DummyProgress)
65
 
            uihack._progress_bar_stack = pbs
 
69
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
66
70
        return uihack._progress_bar_stack.get_nested()
67
71
 
68
72
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
99
103
    out_chunk_list = []
100
104
    for chunk in chunk_list:
101
105
        line_list = []
102
 
        wrap_char = '<wbr/>'
103
106
        delete_list, insert_list = [], []
104
107
        for line in chunk.diff:
105
 
            # Add <wbr/> every X characters so we can wrap properly
106
 
            wrap_line = re.findall(r'.{%d}|.+$' % 78, line.line)
107
 
            wrap_lines = [util.html_clean(_line) for _line in wrap_line]
108
 
            wrapped_line = wrap_char.join(wrap_lines)
109
 
 
110
108
            if line.type == 'context':
111
109
                if len(delete_list) or len(insert_list):
112
 
                    _process_side_by_side_buffers(line_list, delete_list,
113
 
                                                  insert_list)
 
110
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
114
111
                    delete_list, insert_list = [], []
115
 
                line_list.append(util.Container(old_lineno=line.old_lineno,
116
 
                                                new_lineno=line.new_lineno,
117
 
                                                old_line=wrapped_line,
118
 
                                                new_line=wrapped_line,
119
 
                                                old_type=line.type,
120
 
                                                new_type=line.type))
 
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))
121
115
            elif line.type == 'delete':
122
 
                delete_list.append((line.old_lineno, wrapped_line, line.type))
 
116
                delete_list.append((line.old_lineno, line.line, line.type))
123
117
            elif line.type == 'insert':
124
 
                insert_list.append((line.new_lineno, wrapped_line, line.type))
 
118
                insert_list.append((line.new_lineno, line.line, line.type))
125
119
        if len(delete_list) or len(insert_list):
126
120
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
127
121
        out_chunk_list.append(util.Container(diff=line_list))
156
150
 
157
151
    # Make short form of commit message.
158
152
    short_message = message[0]
159
 
    if len(short_message) > 60:
160
 
        short_message = short_message[:60] + '...'
 
153
    if len(short_message) > 80:
 
154
        short_message = short_message[:80] + '...'
161
155
 
162
156
    return message, short_message
163
157
 
170
164
    return path
171
165
 
172
166
 
 
167
 
173
168
# from bzrlib
174
 
 
175
 
 
176
169
class _RevListToTimestamps(object):
177
170
    """This takes a list of revisions, and allows you to bisect by date"""
178
171
 
184
177
 
185
178
    def __getitem__(self, index):
186
179
        """Get the date of the index'd item"""
187
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
188
 
                   self.revid_list[index]).timestamp)
 
180
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
189
181
 
190
182
    def __len__(self):
191
183
        return len(self.revid_list)
192
184
 
193
185
 
194
186
class History (object):
195
 
    """Decorate a branch to provide information for rendering.
196
 
 
197
 
    History objects are expected to be short lived -- when serving a request
198
 
    for a particular branch, open it, read-lock it, wrap a History object
199
 
    around it, serve the request, throw the History object away, unlock the
200
 
    branch and throw it away.
201
 
 
202
 
    :ivar _file_change_cache: xx
203
 
    """
204
 
 
205
 
    def __init__(self, branch, whole_history_data_cache):
206
 
        assert branch.is_locked(), (
207
 
            "Can only construct a History object with a read-locked branch.")
 
187
 
 
188
    def __init__(self):
 
189
        self._change_cache = None
208
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()
209
198
        self._branch = branch
210
 
        self._inventory_cache = {}
211
 
        self._branch_nick = self._branch.get_config().get_nickname()
212
 
        self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
213
 
 
214
 
        self.last_revid = branch.last_revision()
215
 
 
216
 
        whole_history_data = whole_history_data_cache.get(self.last_revid)
217
 
        if whole_history_data is None:
218
 
            whole_history_data = compute_whole_history_data(branch)
219
 
            whole_history_data_cache[self.last_revid] = whole_history_data
220
 
 
221
 
        (self._revision_graph, self._full_history, self._revision_info,
222
 
         self._revno_revid, self._merge_sort, self._where_merged,
223
 
         ) = whole_history_data
 
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
224
246
 
225
247
    def use_file_cache(self, cache):
226
248
        self._file_change_cache = cache
227
249
 
228
 
    @property
229
 
    def has_revisions(self):
230
 
        return not bzrlib.revision.is_null(self.last_revid)
231
 
 
 
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
232
281
    def get_config(self):
233
282
        return self._branch.get_config()
234
283
 
236
285
        if revid not in self._revision_info:
237
286
            # ghost parent?
238
287
            return 'unknown'
239
 
        (seq, revid, merge_depth,
240
 
         revno_str, end_of_merge) = self._revision_info[revid]
 
288
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
241
289
        return revno_str
242
290
 
243
 
    def get_revids_from(self, revid_list, start_revid):
244
 
        """
245
 
        Yield the mainline (wrt start_revid) revisions that merged each
246
 
        revid in revid_list.
247
 
        """
248
 
        if revid_list is None:
249
 
            revid_list = self._full_history
250
 
        revid_set = set(revid_list)
251
 
        revid = start_revid
 
291
    def get_revision_history(self):
 
292
        return self._full_history
252
293
 
253
 
        def introduced_revisions(revid):
254
 
            r = set([revid])
255
 
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
256
 
            i = seq + 1
257
 
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
258
 
                r.add(self._merge_sort[i][1])
259
 
                i += 1
260
 
            return r
261
 
        while 1:
262
 
            if bzrlib.revision.is_null(revid):
263
 
                return
264
 
            if introduced_revisions(revid) & revid_set:
 
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):
265
302
                yield revid
 
303
            if not self._revision_graph.has_key(revid):
 
304
                return
266
305
            parents = self._revision_graph[revid]
267
306
            if len(parents) == 0:
268
307
                return
269
308
            revid = parents[0]
270
309
 
 
310
    @with_branch_lock
271
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
272
314
        # FIXME: would be awesome if we could get, for a folder, the list of
273
 
        # revisions where items within that folder changed.i
274
 
        try:
275
 
            # FIXME: Workaround for bzr versions prior to 1.6b3.
276
 
            # Remove me eventually pretty please  :)
277
 
            w = self._branch.repository.weave_store.get_weave(
278
 
                    file_id, self._branch.repository.get_transaction())
279
 
            w_revids = w.versions()
280
 
            revids = [r for r in self._full_history if r in w_revids]
281
 
        except AttributeError:
282
 
            possible_keys = [(file_id, revid) for revid in self._full_history]
283
 
            get_parent_map = self._branch.repository.texts.get_parent_map
284
 
            # We chunk the requests as this works better with GraphIndex.
285
 
            # See _filter_revisions_touching_file_id in bzrlib/log.py
286
 
            # for more information.
287
 
            revids = []
288
 
            chunk_size = 1000
289
 
            for start in xrange(0, len(possible_keys), chunk_size):
290
 
                next_keys = possible_keys[start:start + chunk_size]
291
 
                revids += [k[1] for k in get_parent_map(next_keys)]
292
 
            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]
293
319
        return revids
294
320
 
 
321
    @with_branch_lock
295
322
    def get_revision_history_since(self, revid_list, date):
296
323
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
297
324
        # so start at midnight on 02-sep.
298
325
        date = date + datetime.timedelta(days=1)
299
 
        # our revid list is sorted in REVERSE date order,
300
 
        # so go thru some hoops here...
 
326
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
301
327
        revid_list.reverse()
302
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
303
 
                                                   self._branch.repository),
304
 
                              date)
 
328
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
305
329
        if index == 0:
306
330
            return []
307
331
        revid_list.reverse()
308
332
        index = -index
309
333
        return revid_list[index:]
310
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
311
361
    def get_search_revid_list(self, query, revid_list):
312
362
        """
313
363
        given a "quick-search" query, try a few obvious possible meanings:
314
364
 
315
365
            - revision id or # ("128.1.3")
316
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
317
 
iso style "yyyy-mm-dd")
 
366
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
318
367
            - comment text as a fallback
319
368
 
320
369
        and return a revid list that matches.
323
372
        # all the relevant changes (time-consuming) only to return a list of
324
373
        # revids which will be used to fetch a set of changes again.
325
374
 
326
 
        # if they entered a revid, just jump straight there;
327
 
        # ignore the passed-in revid_list
 
375
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
328
376
        revid = self.fix_revid(query)
329
377
        if revid is not None:
330
378
            if isinstance(revid, unicode):
331
379
                revid = revid.encode('utf-8')
332
 
            changes = self.get_changes([revid])
 
380
            changes = self.get_changes([ revid ])
333
381
            if (changes is not None) and (len(changes) > 0):
334
 
                return [revid]
 
382
                return [ revid ]
335
383
 
336
384
        date = None
337
385
        m = self.us_date_re.match(query)
338
386
        if m is not None:
339
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
340
 
                                     int(m.group(1)),
341
 
                                     int(m.group(2)))
 
387
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
342
388
        else:
343
389
            m = self.earth_date_re.match(query)
344
390
            if m is not None:
345
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
346
 
                                         int(m.group(2)),
347
 
                                         int(m.group(1)))
 
391
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
348
392
            else:
349
393
                m = self.iso_date_re.match(query)
350
394
                if m is not None:
351
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
352
 
                                             int(m.group(2)),
353
 
                                             int(m.group(3)))
 
395
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
354
396
        if date is not None:
355
397
            if revid_list is None:
356
 
                # if no limit to the query was given,
357
 
                # search only the direct-parent path.
358
 
                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))
359
400
            return self.get_revision_history_since(revid_list, date)
360
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
 
361
407
    revno_re = re.compile(r'^[\d\.]+$')
362
408
    # the date regex are without a final '$' so that queries like
363
409
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
371
417
        if revid is None:
372
418
            return revid
373
419
        if revid == 'head:':
374
 
            return self.last_revid
375
 
        try:
376
 
            if self.revno_re.match(revid):
377
 
                revid = self._revno_revid[revid]
378
 
        except KeyError:
379
 
            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]
380
423
        return revid
381
424
 
 
425
    @with_branch_lock
382
426
    def get_file_view(self, revid, file_id):
383
427
        """
384
428
        Given a revid and optional path, return a (revlist, revid) for
388
432
        If file_id is None, the entire revision history is the list scope.
389
433
        """
390
434
        if revid is None:
391
 
            revid = self.last_revid
 
435
            revid = self._last_revid
392
436
        if file_id is not None:
393
437
            # since revid is 'start_revid', possibly should start the path
394
438
            # tracing from revid... FIXME
398
442
            revlist = list(self.get_revids_from(None, revid))
399
443
        return revlist
400
444
 
 
445
    @with_branch_lock
401
446
    def get_view(self, revid, start_revid, file_id, query=None):
402
447
        """
403
448
        use the URL parameters (revid, start_revid, file_id, and query) to
404
449
        determine the revision list we're viewing (start_revid, file_id, query)
405
450
        and where we are in it (revid).
406
451
 
407
 
            - if a query is given, we're viewing query results.
408
 
            - if a file_id is given, we're viewing revisions for a specific
409
 
              file.
410
 
            - if a start_revid is given, we're viewing the branch from a
411
 
              specific revision up the tree.
412
 
 
413
 
        these may be combined to view revisions for a specific file, from
414
 
        a specific revision, with a specific search query.
415
 
 
416
 
        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:
417
460
 
418
461
            - revid: current position within the view
419
462
            - start_revid: starting revision of this view
423
466
        contain vital context for future url navigation.
424
467
        """
425
468
        if start_revid is None:
426
 
            start_revid = self.last_revid
 
469
            start_revid = self._last_revid
427
470
 
428
471
        if query is None:
429
472
            revid_list = self.get_file_view(start_revid, file_id)
432
475
            if revid not in revid_list:
433
476
                # if the given revid is not in the revlist, use a revlist that
434
477
                # starts at the given revid.
435
 
                revid_list = self.get_file_view(revid, file_id)
 
478
                revid_list= self.get_file_view(revid, file_id)
436
479
                start_revid = revid
437
480
            return revid, start_revid, revid_list
438
481
 
441
484
            revid_list = self.get_file_view(start_revid, file_id)
442
485
        else:
443
486
            revid_list = None
444
 
        revid_list = search.search_revisions(self._branch, query)
445
 
        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:
446
490
            if revid not in revid_list:
447
491
                revid = revid_list[0]
448
492
            return revid, start_revid, revid_list
449
493
        else:
450
 
            # XXX: This should return a message saying that the search could
451
 
            # not be completed due to either missing the plugin or missing a
452
 
            # search index.
 
494
            # no results
453
495
            return None, None, []
454
496
 
 
497
    @with_branch_lock
455
498
    def get_inventory(self, revid):
456
 
        if revid not in self._inventory_cache:
457
 
            self._inventory_cache[revid] = (
458
 
                self._branch.repository.get_revision_inventory(revid))
459
 
        return self._inventory_cache[revid]
 
499
        return self._branch.repository.get_revision_inventory(revid)
460
500
 
 
501
    @with_branch_lock
461
502
    def get_path(self, revid, file_id):
462
503
        if (file_id is None) or (file_id == ''):
463
504
            return ''
464
 
        path = self.get_inventory(revid).id2path(file_id)
 
505
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
465
506
        if (len(path) > 0) and not path.startswith('/'):
466
507
            path = '/' + path
467
508
        return path
468
509
 
 
510
    @with_branch_lock
469
511
    def get_file_id(self, revid, path):
470
512
        if (len(path) > 0) and not path.startswith('/'):
471
513
            path = '/' + path
472
 
        return self.get_inventory(revid).path2id(path)
 
514
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
 
515
 
473
516
 
474
517
    def get_merge_point_list(self, revid):
475
518
        """
509
552
            revnol = revno.split(".")
510
553
            revnos = ".".join(revnol[:-2])
511
554
            revnolast = int(revnol[-1])
512
 
            if revnos in d.keys():
 
555
            if d.has_key(revnos):
513
556
                m = d[revnos][0]
514
557
                if revnolast < m:
515
 
                    d[revnos] = (revnolast, revid)
 
558
                    d[revnos] = ( revnolast, revid )
516
559
            else:
517
 
                d[revnos] = (revnolast, revid)
 
560
                d[revnos] = ( revnolast, revid )
518
561
 
519
 
        return [d[revnos][1] for revnos in d.keys()]
 
562
        return [ d[revnos][1] for revnos in d.keys() ]
520
563
 
521
564
    def get_branch_nicks(self, changes):
522
565
        """
544
587
                else:
545
588
                    p.branch_nick = '(missing)'
546
589
 
 
590
    @with_branch_lock
547
591
    def get_changes(self, revid_list):
548
 
        """Return a list of changes objects for the given revids.
549
 
 
550
 
        Revisions not present and NULL_REVISION will be ignored.
551
 
        """
552
 
        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)
553
596
        if len(changes) == 0:
554
597
            return changes
555
598
 
556
599
        # some data needs to be recalculated each time, because it may
557
600
        # change as new revisions are added.
558
601
        for change in changes:
559
 
            merge_revids = self.simplify_merge_point_list(
560
 
                               self.get_merge_point_list(change.revid))
561
 
            change.merge_points = [
562
 
                util.Container(revid=r,
563
 
                revno=self.get_revno(r)) for r in merge_revids]
564
 
            if len(change.parents) > 0:
565
 
                change.parents = [util.Container(revid=r,
566
 
                    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]
567
604
            change.revno = self.get_revno(change.revid)
568
605
 
569
606
        parity = 0
573
610
 
574
611
        return changes
575
612
 
576
 
    def get_changes_uncached(self, revid_list):
577
 
        # FIXME: deprecated method in getting a null revision
578
 
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
579
 
                            revid_list)
580
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
581
 
                         revid_list)
582
 
        # We need to return the answer in the same order as the input,
583
 
        # less any ghosts.
584
 
        present_revids = [revid for revid in revid_list
585
 
                          if revid in parent_map]
586
 
        rev_list = self._branch.repository.get_revisions(present_revids)
587
 
 
588
 
        return [self._change_from_revision(rev) for rev in rev_list]
589
 
 
590
 
    def _get_deltas_for_revisions_with_trees(self, revisions):
591
 
        """Produce a list of revision deltas.
 
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.
592
626
 
593
627
        Note that the input is a sequence of REVISIONS, not revision_ids.
594
628
        Trees will be held in memory until the generator exits.
595
629
        Each delta is relative to the revision's lefthand predecessor.
596
 
        (This is copied from bzrlib.)
597
630
        """
598
631
        required_trees = set()
599
 
        for revision in revisions:
600
 
            required_trees.add(revision.revid)
601
 
            required_trees.update([p.revid for p in revision.parents[:1]])
 
632
        for entry in entries:
 
633
            required_trees.add(entry.revid)
 
634
            required_trees.update([p.revid for p in entry.parents[:1]])
602
635
        trees = dict((t.get_revision_id(), t) for
603
 
                     t in self._branch.repository.revision_trees(
604
 
                         required_trees))
 
636
                     t in self._branch.repository.revision_trees(required_trees))
605
637
        ret = []
606
 
        for revision in revisions:
607
 
            if not revision.parents:
608
 
                old_tree = self._branch.repository.revision_tree(
609
 
                    bzrlib.revision.NULL_REVISION)
610
 
            else:
611
 
                old_tree = trees[revision.parents[0].revid]
612
 
            tree = trees[revision.revid]
613
 
            ret.append(tree.changes_from(old_tree))
614
 
        return 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()
615
651
 
616
 
    def _change_from_revision(self, revision):
617
 
        """
618
 
        Given a bzrlib Revision, return a processed "change" for use in
619
 
        templates.
620
 
        """
 
652
    def entry_from_revision(self, revision):
621
653
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
622
654
 
623
 
        parents = [util.Container(revid=r,
624
 
                   revno=self.get_revno(r)) for r in revision.parent_ids]
 
655
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
625
656
 
626
657
        message, short_message = clean_message(revision.message)
627
658
 
628
659
        entry = {
629
660
            'revid': revision.revision_id,
630
661
            'date': commit_time,
631
 
            'author': revision.get_apparent_author(),
 
662
            'author': revision.committer,
632
663
            'branch_nick': revision.properties.get('branch-nick', None),
633
664
            'short_comment': short_message,
634
665
            'comment': revision.message,
635
666
            'comment_clean': [util.html_clean(s) for s in message],
636
 
            'parents': revision.parent_ids,
 
667
            'parents': parents,
637
668
        }
638
669
        return util.Container(entry)
639
670
 
 
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
 
640
692
    def get_file_changes_uncached(self, entries):
641
693
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
642
694
 
643
695
        return [self.parse_delta(delta) for delta in delta_list]
644
696
 
 
697
    @with_branch_lock
645
698
    def get_file_changes(self, entries):
646
699
        if self._file_change_cache is None:
647
700
            return self.get_file_changes_uncached(entries)
654
707
        for entry, changes in zip(entries, changes_list):
655
708
            entry.changes = changes
656
709
 
 
710
    @with_branch_lock
657
711
    def get_change_with_diff(self, revid, compare_revid=None):
658
 
        change = self.get_changes([revid])[0]
 
712
        entry = self.get_changes([revid])[0]
659
713
 
660
714
        if compare_revid is None:
661
 
            if change.parents:
662
 
                compare_revid = change.parents[0].revid
 
715
            if entry.parents:
 
716
                compare_revid = entry.parents[0].revid
663
717
            else:
664
718
                compare_revid = 'null:'
665
719
 
667
721
        rev_tree2 = self._branch.repository.revision_tree(revid)
668
722
        delta = rev_tree2.changes_from(rev_tree1)
669
723
 
670
 
        change.changes = self.parse_delta(delta)
671
 
        change.changes.modified = self._parse_diffs(rev_tree1,
672
 
                                                    rev_tree2,
673
 
                                                    delta)
674
 
 
675
 
        return change
676
 
 
 
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
677
731
    def get_file(self, file_id, revid):
678
732
        "returns (path, filename, data)"
679
733
        inv = self.get_inventory(revid)
704
758
        process = []
705
759
        out = []
706
760
 
707
 
        for old_path, new_path, fid, \
708
 
            kind, text_modified, meta_modified in delta.renamed:
 
761
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
709
762
            if text_modified:
710
763
                process.append((old_path, new_path, fid, kind))
711
764
        for path, fid, kind, text_modified, meta_modified in delta.modified:
725
778
                    diff = buffer.getvalue()
726
779
            else:
727
780
                diff = ''
728
 
            out.append(util.Container(
729
 
                          filename=rich_filename(new_path, kind),
730
 
                          file_id=fid,
731
 
                          chunks=self._process_diff(diff),
732
 
                          raw_diff=diff))
 
781
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
733
782
 
734
783
        return out
735
784
 
748
797
                    chunks.append(chunk)
749
798
                chunk = util.Container()
750
799
                chunk.diff = []
751
 
                split_lines = line.split(' ')[1:3]
752
 
                lines = [int(x.split(',')[0][1:]) for x in split_lines]
 
800
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
753
801
                old_lineno = lines[0]
754
802
                new_lineno = lines[1]
755
803
            elif line.startswith(' '):
756
 
                chunk.diff.append(util.Container(old_lineno=old_lineno,
757
 
                                                 new_lineno=new_lineno,
758
 
                                                 type='context',
759
 
                                                 line=line[1:]))
 
804
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
805
                                                 type='context', line=util.fixed_width(line[1:])))
760
806
                old_lineno += 1
761
807
                new_lineno += 1
762
808
            elif line.startswith('+'):
763
 
                chunk.diff.append(util.Container(old_lineno=None,
764
 
                                                 new_lineno=new_lineno,
765
 
                                                 type='insert', line=line[1:]))
 
809
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
810
                                                 type='insert', line=util.fixed_width(line[1:])))
766
811
                new_lineno += 1
767
812
            elif line.startswith('-'):
768
 
                chunk.diff.append(util.Container(old_lineno=old_lineno,
769
 
                                                 new_lineno=None,
770
 
                                                 type='delete', line=line[1:]))
 
813
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
814
                                                 type='delete', line=util.fixed_width(line[1:])))
771
815
                old_lineno += 1
772
816
            else:
773
 
                chunk.diff.append(util.Container(old_lineno=None,
774
 
                                                 new_lineno=None,
775
 
                                                 type='unknown',
776
 
                                                 line=repr(line)))
 
817
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
818
                                                 type='unknown', line=util.fixed_width(repr(line))))
777
819
        if chunk is not None:
778
820
            chunks.append(chunk)
779
821
        return chunks
799
841
            added.append((rich_filename(path, kind), fid))
800
842
 
801
843
        for path, fid, kind, text_modified, meta_modified in delta.modified:
802
 
            modified.append(util.Container(filename=rich_filename(path, kind),
803
 
                                           file_id=fid))
 
844
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
804
845
 
805
 
        for old_path, new_path, fid, kind, text_modified, meta_modified in \
806
 
delta.renamed:
807
 
            renamed.append((rich_filename(old_path, kind),
808
 
                            rich_filename(new_path, kind), fid))
 
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))
809
848
            if meta_modified or text_modified:
810
 
                modified.append(util.Container(
811
 
                    filename=rich_filename(new_path, kind), file_id=fid))
 
849
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
812
850
 
813
851
        for path, fid, kind in delta.removed:
814
852
            removed.append((rich_filename(path, kind), fid))
815
853
 
816
 
        return util.Container(added=added, renamed=renamed,
817
 
                              removed=removed, modified=modified)
 
854
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
818
855
 
819
856
    @staticmethod
820
857
    def add_side_by_side(changes):
823
860
            for m in change.changes.modified:
824
861
                m.sbs_chunks = _make_side_by_side(m.chunks)
825
862
 
 
863
    @with_branch_lock
826
864
    def get_filelist(self, inv, file_id, sort_type=None):
827
865
        """
828
866
        return the list of all files (and their attributes) within a given
846
884
            pathname = filename
847
885
            if entry.kind == 'directory':
848
886
                pathname += '/'
849
 
            if path == '':
850
 
                absolutepath = pathname
851
 
            else:
852
 
                absolutepath = urllib.quote(path + '/' + pathname)
 
887
 
853
888
            revid = entry.revision
854
889
 
855
890
            file = util.Container(
856
 
                filename=filename, executable=entry.executable,
857
 
                kind=entry.kind, pathname=pathname, absolutepath=absolutepath,
858
 
                file_id=entry.file_id, size=entry.text_size, revid=revid,
859
 
                change=change_dict[revid])
 
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])
860
894
            file_list.append(file)
861
895
 
862
896
        if sort_type == 'filename' or sort_type is None:
863
 
            file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
 
897
            file_list.sort(key=lambda x: x.filename)
864
898
        elif sort_type == 'size':
865
899
            file_list.sort(key=lambda x: x.size)
866
900
        elif sort_type == 'date':
867
901
            file_list.sort(key=lambda x: x.change.date)
868
902
 
869
 
        # Always sort by kind to get directories first
870
 
        file_list.sort(key=lambda x: x.kind != 'directory')
871
 
 
872
903
        parity = 0
873
904
        for file in file_list:
874
905
            file.parity = parity
876
907
 
877
908
        return file_list
878
909
 
 
910
 
 
911
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
 
912
 
 
913
    @with_branch_lock
879
914
    def annotate_file(self, file_id, revid):
880
915
        z = time.time()
881
916
        lineno = 1
883
918
 
884
919
        file_revid = self.get_inventory(revid)[file_id].revision
885
920
        oldvalues = None
886
 
        tree = self._branch.repository.revision_tree(file_revid)
 
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
 
887
927
        revid_set = set()
888
 
 
889
 
        try:
890
 
            bzrlib.textfile.check_text_lines(tree.get_file_lines(file_id))
891
 
        except bzrlib.errors.BinaryFile:
 
928
        for line_revid, text in w.annotate_iter(file_revid):
 
929
            revid_set.add(line_revid)
 
930
            if self._BADCHARS_RE.match(text):
892
931
                # bail out; this isn't displayable text
893
932
                yield util.Container(parity=0, lineno=1, status='same',
894
933
                                     text='(This is a binary file.)',
895
934
                                     change=util.Container())
896
 
        else:
897
 
            for line_revid, text in tree.annotate_iter(file_id):
898
 
                revid_set.add(line_revid)
899
 
 
900
 
            change_cache = dict([(c.revid, c) \
901
 
                    for c in self.get_changes(list(revid_set))])
902
 
 
903
 
            last_line_revid = None
904
 
            for line_revid, text in tree.annotate_iter(file_id):
905
 
                if line_revid == last_line_revid:
906
 
                    # remember which lines have a new revno and which don't
907
 
                    status = 'same'
908
 
                else:
909
 
                    status = 'changed'
910
 
                    parity ^= 1
911
 
                    last_line_revid = line_revid
912
 
                    change = change_cache[line_revid]
913
 
                    trunc_revno = change.revno
914
 
                    if len(trunc_revno) > 10:
915
 
                        trunc_revno = trunc_revno[:9] + '...'
916
 
 
917
 
                yield util.Container(parity=parity, lineno=lineno, status=status,
918
 
                                     change=change, text=util.fixed_width(text))
919
 
                lineno += 1
920
 
 
921
 
        self.log.debug('annotate: %r secs' % (time.time() - z))
 
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