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