~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

by accident, i noticed today that loggerhead is sometimes leaving a lot of
file descriptors open.  i think they all come from this one piece of code
that could potentially return without closing the changes cache.  grr.

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