~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2007-10-29 16:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 141.
  • Revision ID: michael.hudson@canonical.com-20071029161930-oxqrd4rd8j1oz3hx
add do nothing check target

Show diffs side-by-side

added added

removed removed

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