~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: John Arbash Meinel
  • Date: 2008-07-26 14:52:44 UTC
  • mto: This revision was merged to the branch mainline in revision 185.
  • Revision ID: john@arbash-meinel.com-20080726145244-l7h1ndtlu5mnm9tg
Add Copyright information to most files.

Fix the documentation for start/stop in the README.txt

Show diffs side-by-side

added added

removed removed

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