~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

it's called loggerhead :)

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
 
28
28
 
29
29
import bisect
 
30
import cgi
30
31
import datetime
31
32
import logging
 
33
import os
 
34
import posixpath
32
35
import re
 
36
import shelve
 
37
import sys
33
38
import textwrap
34
39
import threading
35
40
import time
39
44
from loggerhead.util import decorator
40
45
 
41
46
import bzrlib
 
47
import bzrlib.annotate
42
48
import bzrlib.branch
43
49
import bzrlib.bundle.serializer
 
50
import bzrlib.decorators
44
51
import bzrlib.diff
45
52
import bzrlib.errors
46
53
import bzrlib.progress
47
 
import bzrlib.revision
 
54
import bzrlib.textfile
48
55
import bzrlib.tsort
49
56
import bzrlib.ui
50
57
 
51
58
 
52
59
with_branch_lock = util.with_lock('_lock', 'branch')
53
60
 
54
 
 
55
61
@decorator
56
62
def with_bzrlib_read_lock(unbound):
57
63
    def bzrlib_read_locked(self, *args, **kw):
94
100
    """
95
101
    turn a normal unified-style diff (post-processed by parse_delta) into a
96
102
    side-by-side diff structure.  the new structure is::
97
 
 
 
103
    
98
104
        chunks: list(
99
105
            diff: list(
100
106
                old_lineno: int,
136
142
 
137
143
 
138
144
def clean_message(message):
139
 
    """Clean up a commit message and return it and a short (1-line) version.
140
 
 
141
 
    Commit messages that are long single lines are reflowed using the textwrap
142
 
    module (Robey, the original author of this code, apparently favored this
143
 
    style of message).
144
 
    """
 
145
    # clean up a commit message and return it and a short (1-line) version
145
146
    message = message.splitlines()
146
 
 
147
147
    if len(message) == 1:
 
148
        # robey-style 1-line long message
148
149
        message = textwrap.wrap(message[0])
149
 
 
150
 
    if len(message) == 0:
151
 
        # We can end up where when (a) the commit message was empty or (b)
152
 
        # when the message consisted entirely of whitespace, in which case
153
 
        # textwrap.wrap() returns an empty list.
154
 
        return [''], ''
155
 
 
156
 
    # Make short form of commit message.
 
150
    elif len(message) == 0:
 
151
        # sometimes a commit may have NO message!
 
152
        message = ['']
 
153
        
 
154
    # make short form of commit message
157
155
    short_message = message[0]
158
156
    if len(short_message) > 60:
159
157
        short_message = short_message[:60] + '...'
160
 
 
 
158
    
161
159
    return message, short_message
162
160
 
163
161
 
164
 
def rich_filename(path, kind):
165
 
    if kind == 'directory':
166
 
        path += '/'
167
 
    if kind == 'symlink':
168
 
        path += '@'
169
 
    return path
170
 
 
171
 
 
172
 
 
173
162
# from bzrlib
174
163
class _RevListToTimestamps(object):
175
164
    """This takes a list of revisions, and allows you to bisect by date"""
189
178
 
190
179
 
191
180
class History (object):
192
 
 
 
181
    
193
182
    def __init__(self):
194
 
        self._file_change_cache = None
 
183
        self._change_cache = None
 
184
        self._index = None
195
185
        self._lock = threading.RLock()
196
 
 
 
186
    
197
187
    @classmethod
198
 
    def from_branch(cls, branch):
 
188
    def from_branch(cls, branch, name=None):
199
189
        z = time.time()
200
190
        self = cls()
201
191
        self._branch = branch
202
 
        self._last_revid = self._branch.last_revision()
203
 
 
204
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch.nick,))
205
 
 
206
 
        graph = branch.repository.get_graph()
207
 
        parent_map = dict(((key, value) for key, value in
208
 
             graph.iter_ancestry([self._last_revid]) if value is not None))
209
 
 
210
 
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
 
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
        
211
201
        self._full_history = []
212
202
        self._revision_info = {}
213
203
        self._revno_revid = {}
214
 
        if bzrlib.revision.is_null(self._last_revid):
215
 
            self._merge_sort = []
216
 
        else:
217
 
            self._merge_sort = bzrlib.tsort.merge_sort(
218
 
                self._revision_graph, self._last_revid, generate_revno=True)
219
 
 
 
204
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
 
205
        count = 0
220
206
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
221
207
            self._full_history.append(revid)
222
208
            revno_str = '.'.join(str(n) for n in revno)
223
209
            self._revno_revid[revno_str] = revid
224
 
            self._revision_info[revid] = (
225
 
                seq, revid, merge_depth, revno_str, end_of_merge)
 
210
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
 
211
            count += 1
 
212
        self._count = count
226
213
 
227
214
        # cache merge info
228
215
        self._where_merged = {}
229
 
 
230
216
        for revid in self._revision_graph.keys():
231
 
            if self._revision_info[revid][2] == 0:
 
217
            if not revid in self._full_history: 
232
218
                continue
233
219
            for parent in self._revision_graph[revid]:
234
220
                self._where_merged.setdefault(parent, set()).add(revid)
235
221
 
236
222
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
237
223
        return self
238
 
 
239
 
    @staticmethod
240
 
    def _strip_NULL_ghosts(revision_graph):
241
 
        """
242
 
        Copied over from bzrlib meant as a temporary workaround deprecated 
243
 
        methods.
244
 
        """
245
 
 
246
 
        # Filter ghosts, and null:
247
 
        if bzrlib.revision.NULL_REVISION in revision_graph:
248
 
            del revision_graph[bzrlib.revision.NULL_REVISION]
249
 
        for key, parents in revision_graph.items():
250
 
            revision_graph[key] = tuple(parent for parent in parents if parent
251
 
                in revision_graph)
252
 
        return revision_graph
253
 
 
 
224
    
254
225
    @classmethod
255
 
    def from_folder(cls, path):
 
226
    def from_folder(cls, path, name=None):
256
227
        b = bzrlib.branch.Branch.open(path)
257
 
        b.lock_read()
258
 
        try:
259
 
            return cls.from_branch(b)
260
 
        finally:
261
 
            b.unlock()
 
228
        return cls.from_branch(b, name)
262
229
 
263
230
    @with_branch_lock
264
231
    def out_of_date(self):
265
 
        # the branch may have been upgraded on disk, in which case we're stale.
266
 
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
267
 
        if self._branch.__class__ is not \
268
 
               newly_opened.__class__:
269
 
            return True
270
 
        if self._branch.repository.__class__ is not \
271
 
               newly_opened.repository.__class__:
272
 
            return True
273
 
        return self._branch.last_revision() != self._last_revid
274
 
 
275
 
    def use_file_cache(self, cache):
276
 
        self._file_change_cache = cache
277
 
 
278
 
    @property
279
 
    def has_revisions(self):
280
 
        return not bzrlib.revision.is_null(self.last_revid)
281
 
 
 
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
    
282
267
    last_revid = property(lambda self: self._last_revid, None, None)
 
268
    
 
269
    count = property(lambda self: self._count, None, None)
283
270
 
284
271
    @with_branch_lock
285
272
    def get_config(self):
286
273
        return self._branch.get_config()
287
 
 
 
274
    
 
275
    @with_branch_lock
 
276
    def get_revision(self, revid):
 
277
        return self._branch.repository.get_revision(revid)
 
278
    
288
279
    def get_revno(self, revid):
289
280
        if revid not in self._revision_info:
290
281
            # ghost parent?
292
283
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
293
284
        return revno_str
294
285
 
 
286
    def get_sequence(self, revid):
 
287
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
288
        return seq
 
289
    
295
290
    def get_revision_history(self):
296
291
        return self._full_history
297
 
 
298
 
    def get_revids_from(self, revid_list, start_revid):
299
 
        """
300
 
        Yield the mainline (wrt start_revid) revisions that merged each
301
 
        revid in revid_list.
302
 
        """
303
 
        if revid_list is None:
304
 
            revid_list = self._full_history
305
 
        revid_set = set(revid_list)
306
 
        revid = start_revid
307
 
        def introduced_revisions(revid):
308
 
            r = set([revid])
309
 
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
310
 
            i = seq + 1
311
 
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
312
 
                r.add(self._merge_sort[i][1])
313
 
                i += 1
314
 
            return r
315
 
        while 1:
316
 
            if bzrlib.revision.is_null(revid):
317
 
                return
318
 
            if introduced_revisions(revid) & revid_set:
 
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):
319
312
                yield revid
 
313
            if not self._revision_graph.has_key(revid):
 
314
                return
320
315
            parents = self._revision_graph[revid]
321
316
            if len(parents) == 0:
322
317
                return
323
318
            revid = parents[0]
324
 
 
 
319
    
325
320
    @with_branch_lock
326
321
    def get_short_revision_history_by_fileid(self, file_id):
327
322
        # wow.  is this really the only way we can get this list?  by
346
341
        revid_list.reverse()
347
342
        index = -index
348
343
        return revid_list[index:]
 
344
    
 
345
    @with_branch_lock
 
346
    def get_revision_history_matching(self, revid_list, text):
 
347
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
348
        z = time.time()
 
349
        # this is going to be painfully slow. :(
 
350
        out = []
 
351
        text = text.lower()
 
352
        for revid in revid_list:
 
353
            change = self.get_changes([ revid ])[0]
 
354
            if text in change.comment.lower():
 
355
                out.append(revid)
 
356
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
 
357
        return out
349
358
 
 
359
    def get_revision_history_matching_indexed(self, revid_list, text):
 
360
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
361
        z = time.time()
 
362
        if self._index is None:
 
363
            return self.get_revision_history_matching(revid_list, text)
 
364
        out = self._index.find(text, revid_list)
 
365
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
 
366
        # put them in some coherent order :)
 
367
        out = [r for r in self._full_history if r in out]
 
368
        return out
 
369
    
350
370
    @with_branch_lock
351
371
    def get_search_revid_list(self, query, revid_list):
352
372
        """
353
373
        given a "quick-search" query, try a few obvious possible meanings:
354
 
 
 
374
        
355
375
            - revision id or # ("128.1.3")
356
376
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
357
377
            - comment text as a fallback
361
381
        # FIXME: there is some silliness in this action.  we have to look up
362
382
        # all the relevant changes (time-consuming) only to return a list of
363
383
        # revids which will be used to fetch a set of changes again.
364
 
 
 
384
        
365
385
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
366
386
        revid = self.fix_revid(query)
367
387
        if revid is not None:
368
 
            if isinstance(revid, unicode):
369
 
                revid = revid.encode('utf-8')
370
388
            changes = self.get_changes([ revid ])
371
389
            if (changes is not None) and (len(changes) > 0):
372
390
                return [ revid ]
373
 
 
 
391
        
374
392
        date = None
375
393
        m = self.us_date_re.match(query)
376
394
        if m is not None:
388
406
                # if no limit to the query was given, search only the direct-parent path.
389
407
                revid_list = list(self.get_revids_from(None, self._last_revid))
390
408
            return self.get_revision_history_since(revid_list, date)
391
 
 
 
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
    
392
415
    revno_re = re.compile(r'^[\d\.]+$')
393
416
    # the date regex are without a final '$' so that queries like
394
417
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
406
429
        if self.revno_re.match(revid):
407
430
            revid = self._revno_revid[revid]
408
431
        return revid
409
 
 
 
432
    
410
433
    @with_branch_lock
411
434
    def get_file_view(self, revid, file_id):
412
435
        """
413
 
        Given a revid and optional path, return a (revlist, revid) for
414
 
        navigation through the current scope: from the revid (or the latest
415
 
        revision) back to the original revision.
416
 
 
 
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
        
417
440
        If file_id is None, the entire revision history is the list scope.
 
441
        If revid is None, the latest revision is used.
418
442
        """
419
443
        if revid is None:
420
444
            revid = self._last_revid
421
445
        if file_id is not None:
422
 
            # since revid is 'start_revid', possibly should start the path
423
 
            # tracing from revid... FIXME
 
446
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
 
447
            inv = self._branch.repository.get_revision_inventory(revid)
424
448
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
425
449
            revlist = list(self.get_revids_from(revlist, revid))
426
450
        else:
427
451
            revlist = list(self.get_revids_from(None, revid))
428
 
        return revlist
429
 
 
 
452
        if revid is None:
 
453
            revid = revlist[0]
 
454
        return revlist, revid
 
455
    
430
456
    @with_branch_lock
431
457
    def get_view(self, revid, start_revid, file_id, query=None):
432
458
        """
433
459
        use the URL parameters (revid, start_revid, file_id, and query) to
434
460
        determine the revision list we're viewing (start_revid, file_id, query)
435
461
        and where we are in it (revid).
436
 
 
437
 
            - if a query is given, we're viewing query results.
438
 
            - if a file_id is given, we're viewing revisions for a specific
439
 
              file.
440
 
            - if a start_revid is given, we're viewing the branch from a
441
 
              specific revision up the tree.
442
 
 
443
 
        these may be combined to view revisions for a specific file, from
444
 
        a specific revision, with a specific search query.
445
 
 
446
 
        returns a new (revid, start_revid, revid_list) where:
447
 
 
 
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
        
448
472
            - revid: current position within the view
449
473
            - start_revid: starting revision of this view
450
474
            - revid_list: list of revision ids for this view
451
 
 
 
475
        
452
476
        file_id and query are never changed so aren't returned, but they may
453
477
        contain vital context for future url navigation.
454
478
        """
455
 
        if start_revid is None:
456
 
            start_revid = self._last_revid
457
 
 
458
479
        if query is None:
459
 
            revid_list = self.get_file_view(start_revid, file_id)
 
480
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
460
481
            if revid is None:
461
482
                revid = start_revid
462
483
            if revid not in revid_list:
463
484
                # if the given revid is not in the revlist, use a revlist that
464
485
                # starts at the given revid.
465
 
                revid_list = self.get_file_view(revid, file_id)
466
 
                start_revid = revid
 
486
                revid_list, start_revid = self.get_file_view(revid, file_id)
467
487
            return revid, start_revid, revid_list
468
 
 
 
488
        
469
489
        # potentially limit the search
470
 
        if file_id is not None:
471
 
            revid_list = self.get_file_view(start_revid, file_id)
 
490
        if (start_revid is not None) or (file_id is not None):
 
491
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
472
492
        else:
473
493
            revid_list = None
474
494
 
475
495
        revid_list = self.get_search_revid_list(query, revid_list)
476
 
        if revid_list and len(revid_list) > 0:
 
496
        if len(revid_list) > 0:
477
497
            if revid not in revid_list:
478
498
                revid = revid_list[0]
479
499
            return revid, start_revid, revid_list
480
500
        else:
 
501
            # no results
481
502
            return None, None, []
482
503
 
483
504
    @with_branch_lock
492
513
        if (len(path) > 0) and not path.startswith('/'):
493
514
            path = '/' + path
494
515
        return path
495
 
 
 
516
    
496
517
    @with_branch_lock
497
518
    def get_file_id(self, revid, path):
498
519
        if (len(path) > 0) and not path.startswith('/'):
499
520
            path = '/' + path
500
521
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
501
 
 
 
522
    
 
523
    def get_where_merged(self, revid):
 
524
        try:
 
525
            return self._where_merged[revid]
 
526
        except:
 
527
            return []
 
528
    
502
529
    def get_merge_point_list(self, revid):
503
530
        """
504
531
        Return the list of revids that have merged this node.
505
532
        """
506
 
        if '.' not in self.get_revno(revid):
 
533
        if revid in self._history:
507
534
            return []
508
 
 
 
535
        
509
536
        merge_point = []
510
537
        while True:
511
 
            children = self._where_merged.get(revid, [])
 
538
            children = self.get_where_merged(revid)
512
539
            nexts = []
513
540
            for child in children:
514
541
                child_parents = self._revision_graph[child]
528
555
                merge_point.extend(merge_point_next)
529
556
 
530
557
            revid = nexts[0]
531
 
 
 
558
            
532
559
    def simplify_merge_point_list(self, revids):
533
560
        """if a revision is already merged, don't show further merge points"""
534
561
        d = {}
571
598
                    p.branch_nick = p_change_dict[p.revid].branch_nick
572
599
                else:
573
600
                    p.branch_nick = '(missing)'
574
 
 
 
601
    
575
602
    @with_branch_lock
576
 
    def get_changes(self, revid_list):
577
 
        """Return a list of changes objects for the given revids.
578
 
 
579
 
        Revisions not present and NULL_REVISION will be ignored.
580
 
        """
581
 
        changes = self.get_changes_uncached(revid_list)
582
 
        if len(changes) == 0:
 
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:
583
609
            return changes
584
 
 
 
610
        
585
611
        # some data needs to be recalculated each time, because it may
586
612
        # change as new revisions are added.
587
 
        for change in changes:
588
 
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
 
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))
589
617
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
590
 
            if len(change.parents) > 0:
591
 
                change.parents = [util.Container(revid=r, 
592
 
                    revno=self.get_revno(r)) for r in change.parents]
593
 
            change.revno = self.get_revno(change.revid)
594
 
 
595
 
        parity = 0
596
 
        for change in changes:
597
 
            change.parity = parity
598
 
            parity ^= 1
599
 
 
 
618
        
600
619
        return changes
601
620
 
602
 
    @with_branch_lock
603
 
    @with_bzrlib_read_lock
604
 
    def get_changes_uncached(self, revid_list):
605
 
        # FIXME: deprecated method in getting a null revision
606
 
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
607
 
                            revid_list)
608
 
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
609
 
        # We need to return the answer in the same order as the input,
610
 
        # less any ghosts.
611
 
        present_revids = [revid for revid in revid_list
612
 
                          if revid in parent_map]
613
 
        rev_list = self._branch.repository.get_revisions(present_revids)
614
 
 
615
 
        return [self._change_from_revision(rev) for rev in rev_list]
 
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
616
631
 
617
632
    def _get_deltas_for_revisions_with_trees(self, revisions):
618
 
        """Produce a list of revision deltas.
619
 
 
 
633
        """Produce a generator of revision deltas.
 
634
        
620
635
        Note that the input is a sequence of REVISIONS, not revision_ids.
621
636
        Trees will be held in memory until the generator exits.
622
637
        Each delta is relative to the revision's lefthand predecessor.
623
 
        (This is copied from bzrlib.)
624
638
        """
625
639
        required_trees = set()
626
640
        for revision in revisions:
627
 
            required_trees.add(revision.revid)
628
 
            required_trees.update([p.revid for p in revision.parents[:1]])
629
 
        trees = dict((t.get_revision_id(), t) for
 
641
            required_trees.add(revision.revision_id)
 
642
            required_trees.update(revision.parent_ids[:1])
 
643
        trees = dict((t.get_revision_id(), t) for 
630
644
                     t in self._branch.repository.revision_trees(required_trees))
631
645
        ret = []
632
646
        self._branch.repository.lock_read()
633
647
        try:
634
648
            for revision in revisions:
635
 
                if not revision.parents:
636
 
                    old_tree = self._branch.repository.revision_tree(
637
 
                        bzrlib.revision.NULL_REVISION)
 
649
                if not revision.parent_ids:
 
650
                    old_tree = self._branch.repository.revision_tree(None)
638
651
                else:
639
 
                    old_tree = trees[revision.parents[0].revid]
640
 
                tree = trees[revision.revid]
641
 
                ret.append(tree.changes_from(old_tree))
 
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)))
642
655
            return ret
643
656
        finally:
644
657
            self._branch.repository.unlock()
645
 
 
646
 
    def _change_from_revision(self, revision):
647
 
        """
648
 
        Given a bzrlib Revision, return a processed "change" for use in
649
 
        templates.
650
 
        """
 
658
    
 
659
    def entry_from_revision(self, revision):
651
660
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
652
 
 
 
661
        
653
662
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
654
663
 
 
664
        if len(parents) == 0:
 
665
            left_parent = None
 
666
        else:
 
667
            left_parent = revision.parent_ids[0]
 
668
        
655
669
        message, short_message = clean_message(revision.message)
656
670
 
657
671
        entry = {
658
672
            'revid': revision.revision_id,
 
673
            'revno': self.get_revno(revision.revision_id),
659
674
            'date': commit_time,
660
675
            'author': revision.committer,
661
676
            'branch_nick': revision.properties.get('branch-nick', None),
662
677
            'short_comment': short_message,
663
678
            'comment': revision.message,
664
679
            'comment_clean': [util.html_clean(s) for s in message],
665
 
            'parents': revision.parent_ids,
 
680
            'parents': parents,
666
681
        }
667
682
        return util.Container(entry)
668
683
 
669
 
    def get_file_changes_uncached(self, entries):
670
 
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
671
 
 
672
 
        return [self.parse_delta(delta) for delta in delta_list]
673
 
 
674
 
    @with_branch_lock
675
 
    def get_file_changes(self, entries):
676
 
        if self._file_change_cache is None:
677
 
            return self.get_file_changes_uncached(entries)
678
 
        else:
679
 
            return self._file_change_cache.get_file_changes(entries)
680
 
 
681
 
    def add_changes(self, entries):
682
 
        changes_list = self.get_file_changes(entries)
683
 
 
684
 
        for entry, changes in zip(entries, changes_list):
685
 
            entry.changes = changes
686
 
 
687
 
    @with_branch_lock
688
 
    def get_change_with_diff(self, revid, compare_revid=None):
689
 
        change = self.get_changes([revid])[0]
690
 
 
691
 
        if compare_revid is None:
692
 
            if change.parents:
693
 
                compare_revid = change.parents[0].revid
694
 
            else:
695
 
                compare_revid = 'null:'
696
 
 
697
 
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
698
 
        rev_tree2 = self._branch.repository.revision_tree(revid)
 
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)
699
713
        delta = rev_tree2.changes_from(rev_tree1)
700
 
 
701
 
        change.changes = self.parse_delta(delta)
702
 
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
703
 
 
704
 
        return change
705
 
 
 
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
    
706
722
    @with_branch_lock
707
723
    def get_file(self, file_id, revid):
708
724
        "returns (path, filename, data)"
713
729
        if not path.startswith('/'):
714
730
            path = '/' + path
715
731
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
716
 
 
717
 
    def _parse_diffs(self, old_tree, new_tree, delta):
 
732
    
 
733
    @with_branch_lock
 
734
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
718
735
        """
719
 
        Return a list of processed diffs, in the format::
720
 
 
721
 
            list(
 
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(
722
742
                filename: str,
723
743
                file_id: str,
724
744
                chunks: list(
730
750
                    ),
731
751
                ),
732
752
            )
 
753
        
 
754
        if C{get_diffs} is false, the C{chunks} will be omitted.
733
755
        """
734
 
        process = []
735
 
        out = []
736
 
 
737
 
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
738
 
            if text_modified:
739
 
                process.append((old_path, new_path, fid, kind))
740
 
        for path, fid, kind, text_modified, meta_modified in delta.modified:
741
 
            process.append((path, path, fid, kind))
742
 
 
743
 
        for old_path, new_path, fid, kind in process:
 
756
        added = []
 
757
        modified = []
 
758
        renamed = []
 
759
        removed = []
 
760
        
 
761
        def rich_filename(path, kind):
 
762
            if kind == 'directory':
 
763
                path += '/'
 
764
            if kind == 'symlink':
 
765
                path += '@'
 
766
            return path
 
767
        
 
768
        def process_diff(diff):
 
769
            chunks = []
 
770
            chunk = None
 
771
            for line in diff.splitlines():
 
772
                if len(line) == 0:
 
773
                    continue
 
774
                if line.startswith('+++ ') or line.startswith('--- '):
 
775
                    continue
 
776
                if line.startswith('@@ '):
 
777
                    # new chunk
 
778
                    if chunk is not None:
 
779
                        chunks.append(chunk)
 
780
                    chunk = util.Container()
 
781
                    chunk.diff = []
 
782
                    lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
 
783
                    old_lineno = lines[0]
 
784
                    new_lineno = lines[1]
 
785
                elif line.startswith(' '):
 
786
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
787
                                                     type='context', line=util.html_clean(line[1:])))
 
788
                    old_lineno += 1
 
789
                    new_lineno += 1
 
790
                elif line.startswith('+'):
 
791
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
792
                                                     type='insert', line=util.html_clean(line[1:])))
 
793
                    new_lineno += 1
 
794
                elif line.startswith('-'):
 
795
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
796
                                                     type='delete', line=util.html_clean(line[1:])))
 
797
                    old_lineno += 1
 
798
                else:
 
799
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
800
                                                     type='unknown', line=util.html_clean(repr(line))))
 
801
            if chunk is not None:
 
802
                chunks.append(chunk)
 
803
            return chunks
 
804
                    
 
805
        def handle_modify(old_path, new_path, fid, kind):
 
806
            if not get_diffs:
 
807
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
808
                return
744
809
            old_lines = old_tree.get_file_lines(fid)
745
810
            new_lines = new_tree.get_file_lines(fid)
746
811
            buffer = StringIO()
747
 
            if old_lines != new_lines:
748
 
                try:
749
 
                    bzrlib.diff.internal_diff(old_path, old_lines,
750
 
                                              new_path, new_lines, buffer)
751
 
                except bzrlib.errors.BinaryFile:
752
 
                    diff = ''
753
 
                else:
754
 
                    diff = buffer.getvalue()
755
 
            else:
 
812
            try:
 
813
                bzrlib.diff.internal_diff(old_path, old_lines,
 
814
                                          new_path, new_lines, buffer)
 
815
            except bzrlib.errors.BinaryFile:
756
816
                diff = ''
757
 
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
758
 
 
759
 
        return out
760
 
 
761
 
    def _process_diff(self, diff):
762
 
        # doesn't really need to be a method; could be static.
763
 
        chunks = []
764
 
        chunk = None
765
 
        for line in diff.splitlines():
766
 
            if len(line) == 0:
767
 
                continue
768
 
            if line.startswith('+++ ') or line.startswith('--- '):
769
 
                continue
770
 
            if line.startswith('@@ '):
771
 
                # new chunk
772
 
                if chunk is not None:
773
 
                    chunks.append(chunk)
774
 
                chunk = util.Container()
775
 
                chunk.diff = []
776
 
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
777
 
                old_lineno = lines[0]
778
 
                new_lineno = lines[1]
779
 
            elif line.startswith(' '):
780
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
781
 
                                                 type='context', line=util.fixed_width(line[1:])))
782
 
                old_lineno += 1
783
 
                new_lineno += 1
784
 
            elif line.startswith('+'):
785
 
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
786
 
                                                 type='insert', line=util.fixed_width(line[1:])))
787
 
                new_lineno += 1
788
 
            elif line.startswith('-'):
789
 
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
790
 
                                                 type='delete', line=util.fixed_width(line[1:])))
791
 
                old_lineno += 1
792
817
            else:
793
 
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
794
 
                                                 type='unknown', line=util.fixed_width(repr(line))))
795
 
        if chunk is not None:
796
 
            chunks.append(chunk)
797
 
        return chunks
798
 
 
799
 
    def parse_delta(self, delta):
800
 
        """
801
 
        Return a nested data structure containing the changes in a delta::
802
 
 
803
 
            added: list((filename, file_id)),
804
 
            renamed: list((old_filename, new_filename, file_id)),
805
 
            deleted: list((filename, file_id)),
806
 
            modified: list(
807
 
                filename: str,
808
 
                file_id: str,
809
 
            )
810
 
        """
811
 
        added = []
812
 
        modified = []
813
 
        renamed = []
814
 
        removed = []
 
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))
815
820
 
816
821
        for path, fid, kind in delta.added:
817
822
            added.append((rich_filename(path, kind), fid))
818
 
 
 
823
        
819
824
        for path, fid, kind, text_modified, meta_modified in delta.modified:
820
 
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
821
 
 
822
 
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
823
 
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
 
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))
824
829
            if meta_modified or text_modified:
825
 
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
826
 
 
 
830
                handle_modify(oldpath, newpath, fid, kind)
 
831
        
827
832
        for path, fid, kind in delta.removed:
828
833
            removed.append((rich_filename(path, kind), fid))
829
 
 
 
834
        
830
835
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
831
836
 
832
837
    @staticmethod
835
840
        for change in changes:
836
841
            for m in change.changes.modified:
837
842
                m.sbs_chunks = _make_side_by_side(m.chunks)
838
 
 
 
843
    
839
844
    @with_branch_lock
840
 
    def get_filelist(self, inv, file_id, sort_type=None):
 
845
    def get_filelist(self, inv, path, sort_type=None):
841
846
        """
842
847
        return the list of all files (and their attributes) within a given
843
848
        path subtree.
844
849
        """
845
 
 
846
 
        dir_ie = inv[file_id]
847
 
        path = inv.id2path(file_id)
 
850
        while path.endswith('/'):
 
851
            path = path[:-1]
 
852
        if path.startswith('/'):
 
853
            path = path[1:]
 
854
        
 
855
        entries = inv.entries()
 
856
        
848
857
        file_list = []
849
 
 
850
 
        revid_set = set()
851
 
 
852
 
        for filename, entry in dir_ie.children.iteritems():
853
 
            revid_set.add(entry.revision)
854
 
 
855
 
        change_dict = {}
856
 
        for change in self.get_changes(list(revid_set)):
857
 
            change_dict[change.revid] = change
858
 
 
859
 
        for filename, entry in dir_ie.children.iteritems():
 
858
        for filepath, entry in entries:
 
859
            if posixpath.dirname(filepath) != path:
 
860
                continue
 
861
            filename = posixpath.basename(filepath)
 
862
            rich_filename = filename
860
863
            pathname = filename
861
864
            if entry.kind == 'directory':
862
865
                pathname += '/'
863
866
 
864
867
            revid = entry.revision
 
868
            revision = self._branch.repository.get_revision(revid)
865
869
 
866
 
            file = util.Container(
867
 
                filename=filename, executable=entry.executable, kind=entry.kind,
868
 
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
869
 
                revid=revid, change=change_dict[revid])
 
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)
870
875
            file_list.append(file)
871
 
 
872
 
        if sort_type == 'filename' or sort_type is None:
 
876
        
 
877
        if sort_type == 'filename':
873
878
            file_list.sort(key=lambda x: x.filename)
874
879
        elif sort_type == 'size':
875
880
            file_list.sort(key=lambda x: x.size)
876
881
        elif sort_type == 'date':
877
882
            file_list.sort(key=lambda x: x.change.date)
878
 
 
 
883
        
879
884
        parity = 0
880
885
        for file in file_list:
881
886
            file.parity = parity
891
896
        z = time.time()
892
897
        lineno = 1
893
898
        parity = 0
894
 
 
 
899
        
895
900
        file_revid = self.get_inventory(revid)[file_id].revision
896
901
        oldvalues = None
897
 
        tree = self._branch.repository.revision_tree(file_revid)
 
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
        
898
908
        revid_set = set()
899
 
 
900
 
        for line_revid, text in tree.annotate_iter(file_id):
 
909
        for line_revid, text in w.annotate_iter(file_revid):
901
910
            revid_set.add(line_revid)
902
911
            if self._BADCHARS_RE.match(text):
903
912
                # bail out; this isn't displayable text
904
913
                yield util.Container(parity=0, lineno=1, status='same',
905
 
                                     text='(This is a binary file.)',
 
914
                                     text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
906
915
                                     change=util.Container())
907
916
                return
908
 
        change_cache = dict([(c.revid, c) \
909
 
                for c in self.get_changes(list(revid_set))])
910
 
 
 
917
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
 
918
        
911
919
        last_line_revid = None
912
 
        for line_revid, text in tree.annotate_iter(file_id):
 
920
        for line_revid, text in w.annotate_iter(file_revid):
913
921
            if line_revid == last_line_revid:
914
922
                # remember which lines have a new revno and which don't
915
923
                status = 'same'
921
929
                trunc_revno = change.revno
922
930
                if len(trunc_revno) > 10:
923
931
                    trunc_revno = trunc_revno[:9] + '...'
924
 
 
 
932
                
925
933
            yield util.Container(parity=parity, lineno=lineno, status=status,
926
 
                                 change=change, text=util.fixed_width(text))
 
934
                                 change=change, text=util.html_clean(text))
927
935
            lineno += 1
928
 
 
 
936
        
929
937
        self.log.debug('annotate: %r secs' % (time.time() - z,))
930
938
 
931
939
    @with_branch_lock
 
940
    @with_bzrlib_read_lock
932
941
    def get_bundle(self, revid, compare_revid=None):
933
942
        if compare_revid is None:
934
943
            parents = self._revision_graph[revid]
939
948
        s = StringIO()
940
949
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
941
950
        return s.getvalue()
 
951