~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Robey Pointer
  • Date: 2007-01-02 07:34:15 UTC
  • Revision ID: robey@lag.net-20070102073415-372pbbf98t6ht3oo
flesh out examples for the auto_publish feature.

Show diffs side-by-side

added added

removed removed

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