~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:
46
46
import bzrlib
47
47
import bzrlib.annotate
48
48
import bzrlib.branch
 
49
import bzrlib.bundle.serializer
 
50
import bzrlib.decorators
49
51
import bzrlib.diff
50
52
import bzrlib.errors
51
53
import bzrlib.progress
 
54
import bzrlib.revision
52
55
import bzrlib.textfile
53
56
import bzrlib.tsort
54
57
import bzrlib.ui
56
59
 
57
60
with_branch_lock = util.with_lock('_lock', 'branch')
58
61
 
59
 
@decorator
60
 
def with_bzrlib_read_lock(unbound):
61
 
    def bzrlib_read_locked(self, *args, **kw):
62
 
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
63
 
        self._branch.repository.lock_read()
64
 
        try:
65
 
            return unbound(self, *args, **kw)
66
 
        finally:
67
 
            self._branch.repository.unlock()
68
 
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
69
 
    return bzrlib_read_locked
70
 
 
71
62
 
72
63
# bzrlib's UIFactory is not thread-safe
73
64
uihack = threading.local()
81
72
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
82
73
 
83
74
 
 
75
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
 
76
    while len(delete_list) < len(insert_list):
 
77
        delete_list.append((None, '', 'context'))
 
78
    while len(insert_list) < len(delete_list):
 
79
        insert_list.append((None, '', 'context'))
 
80
    while len(delete_list) > 0:
 
81
        d = delete_list.pop(0)
 
82
        i = insert_list.pop(0)
 
83
        line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
 
84
                                        old_line=d[1], new_line=i[1],
 
85
                                        old_type=d[2], new_type=i[2]))
 
86
 
 
87
 
 
88
def _make_side_by_side(chunk_list):
 
89
    """
 
90
    turn a normal unified-style diff (post-processed by parse_delta) into a
 
91
    side-by-side diff structure.  the new structure is::
 
92
 
 
93
        chunks: list(
 
94
            diff: list(
 
95
                old_lineno: int,
 
96
                new_lineno: int,
 
97
                old_line: str,
 
98
                new_line: str,
 
99
                type: str('context' or 'changed'),
 
100
            )
 
101
        )
 
102
    """
 
103
    out_chunk_list = []
 
104
    for chunk in chunk_list:
 
105
        line_list = []
 
106
        delete_list, insert_list = [], []
 
107
        for line in chunk.diff:
 
108
            if line.type == 'context':
 
109
                if len(delete_list) or len(insert_list):
 
110
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
111
                    delete_list, insert_list = [], []
 
112
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
 
113
                                                old_line=line.line, new_line=line.line,
 
114
                                                old_type=line.type, new_type=line.type))
 
115
            elif line.type == 'delete':
 
116
                delete_list.append((line.old_lineno, line.line, line.type))
 
117
            elif line.type == 'insert':
 
118
                insert_list.append((line.new_lineno, line.line, line.type))
 
119
        if len(delete_list) or len(insert_list):
 
120
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
121
        out_chunk_list.append(util.Container(diff=line_list))
 
122
    return out_chunk_list
 
123
 
 
124
 
 
125
def is_branch(folder):
 
126
    try:
 
127
        bzrlib.branch.Branch.open(folder)
 
128
        return True
 
129
    except:
 
130
        return False
 
131
 
 
132
 
 
133
def clean_message(message):
 
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
    """
 
140
    message = message.splitlines()
 
141
 
 
142
    if len(message) == 1:
 
143
        message = textwrap.wrap(message[0])
 
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.
 
152
    short_message = message[0]
 
153
    if len(short_message) > 80:
 
154
        short_message = short_message[:80] + '...'
 
155
 
 
156
    return message, short_message
 
157
 
 
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
 
84
168
# from bzrlib
85
169
class _RevListToTimestamps(object):
86
170
    """This takes a list of revisions, and allows you to bisect by date"""
100
184
 
101
185
 
102
186
class History (object):
103
 
    
 
187
 
104
188
    def __init__(self):
105
189
        self._change_cache = None
 
190
        self._file_change_cache = None
106
191
        self._index = None
107
192
        self._lock = threading.RLock()
108
 
    
 
193
 
109
194
    @classmethod
110
195
    def from_branch(cls, branch, name=None):
111
196
        z = time.time()
112
197
        self = cls()
113
198
        self._branch = branch
114
 
        self._history = branch.revision_history()
115
 
        self._last_revid = self._history[-1]
116
 
        self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
117
 
        
 
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
 
118
205
        if name is None:
119
206
            name = self._branch.nick
120
207
        self._name = name
121
208
        self.log = logging.getLogger('loggerhead.%s' % (name,))
122
 
        
 
209
 
123
210
        self._full_history = []
124
211
        self._revision_info = {}
125
212
        self._revno_revid = {}
126
213
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
127
 
        count = 0
128
214
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
129
215
            self._full_history.append(revid)
130
216
            revno_str = '.'.join(str(n) for n in revno)
131
217
            self._revno_revid[revno_str] = revid
132
218
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
133
 
            count += 1
134
 
        self._count = count
135
219
 
136
220
        # cache merge info
137
221
        self._where_merged = {}
138
222
        for revid in self._revision_graph.keys():
139
 
            if not revid in self._full_history: 
 
223
            if not revid in self._full_history:
140
224
                continue
141
225
            for parent in self._revision_graph[revid]:
142
226
                self._where_merged.setdefault(parent, set()).add(revid)
143
227
 
144
228
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
145
229
        return self
146
 
    
 
230
 
147
231
    @classmethod
148
232
    def from_folder(cls, path, name=None):
149
233
        b = bzrlib.branch.Branch.open(path)
151
235
 
152
236
    @with_branch_lock
153
237
    def out_of_date(self):
154
 
        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__:
155
241
            return True
156
 
        return False
 
242
        return self._branch.last_revision() != self._last_revid
157
243
 
158
244
    def use_cache(self, cache):
159
245
        self._change_cache = cache
160
 
    
 
246
 
 
247
    def use_file_cache(self, cache):
 
248
        self._file_change_cache = cache
 
249
 
161
250
    def use_search_index(self, index):
162
251
        self._index = index
163
252
 
167
256
        # branch history has changed.  we need to immediately close and stop
168
257
        # using our caches, because a new history object will be created to
169
258
        # replace us, using the same cache files.
 
259
        # (may also be called during server shutdown.)
170
260
        if self._change_cache is not None:
171
261
            self._change_cache.close()
172
262
            self._change_cache = None
178
268
        if self._change_cache is None:
179
269
            return
180
270
        self._change_cache.flush()
181
 
    
 
271
 
182
272
    def check_rebuild(self):
183
273
        if self._change_cache is not None:
184
274
            self._change_cache.check_rebuild()
185
275
        if self._index is not None:
186
276
            self._index.check_rebuild()
187
 
    
 
277
 
188
278
    last_revid = property(lambda self: self._last_revid, None, None)
189
 
    
190
 
    count = property(lambda self: self._count, None, None)
191
279
 
192
280
    @with_branch_lock
193
 
    def get_revision(self, revid):
194
 
        return self._branch.repository.get_revision(revid)
195
 
    
 
281
    def get_config(self):
 
282
        return self._branch.get_config()
 
283
 
196
284
    def get_revno(self, revid):
197
285
        if revid not in self._revision_info:
198
286
            # ghost parent?
200
288
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
201
289
        return revno_str
202
290
 
203
 
    def get_sequence(self, revid):
204
 
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
205
 
        return seq
206
 
    
207
291
    def get_revision_history(self):
208
292
        return self._full_history
209
 
    
210
 
    def get_revid_sequence(self, revid_list, revid):
211
 
        """
212
 
        given a list of revision ids, return the sequence # of this revid in
213
 
        the list.
214
 
        """
215
 
        seq = 0
216
 
        for r in revid_list:
217
 
            if revid == r:
218
 
                return seq
219
 
            seq += 1
220
 
    
 
293
 
221
294
    def get_revids_from(self, revid_list, revid):
222
295
        """
223
296
        given a list of revision ids, yield revisions in graph order,
233
306
            if len(parents) == 0:
234
307
                return
235
308
            revid = parents[0]
236
 
    
 
309
 
237
310
    @with_branch_lock
238
311
    def get_short_revision_history_by_fileid(self, file_id):
239
312
        # wow.  is this really the only way we can get this list?  by
258
331
        revid_list.reverse()
259
332
        index = -index
260
333
        return revid_list[index:]
261
 
    
 
334
 
262
335
    @with_branch_lock
263
336
    def get_revision_history_matching(self, revid_list, text):
264
337
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
283
356
        # put them in some coherent order :)
284
357
        out = [r for r in self._full_history if r in out]
285
358
        return out
286
 
    
 
359
 
287
360
    @with_branch_lock
288
361
    def get_search_revid_list(self, query, revid_list):
289
362
        """
290
363
        given a "quick-search" query, try a few obvious possible meanings:
291
 
        
 
364
 
292
365
            - revision id or # ("128.1.3")
293
366
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
294
367
            - comment text as a fallback
298
371
        # FIXME: there is some silliness in this action.  we have to look up
299
372
        # all the relevant changes (time-consuming) only to return a list of
300
373
        # revids which will be used to fetch a set of changes again.
301
 
        
 
374
 
302
375
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
303
376
        revid = self.fix_revid(query)
304
377
        if revid is not None:
 
378
            if isinstance(revid, unicode):
 
379
                revid = revid.encode('utf-8')
305
380
            changes = self.get_changes([ revid ])
306
381
            if (changes is not None) and (len(changes) > 0):
307
382
                return [ revid ]
308
 
        
 
383
 
309
384
        date = None
310
385
        m = self.us_date_re.match(query)
311
386
        if m is not None:
323
398
                # if no limit to the query was given, search only the direct-parent path.
324
399
                revid_list = list(self.get_revids_from(None, self._last_revid))
325
400
            return self.get_revision_history_since(revid_list, date)
326
 
        
 
401
 
327
402
        # check comment fields.
328
403
        if revid_list is None:
329
404
            revid_list = self._full_history
330
405
        return self.get_revision_history_matching_indexed(revid_list, query)
331
 
    
 
406
 
332
407
    revno_re = re.compile(r'^[\d\.]+$')
333
408
    # the date regex are without a final '$' so that queries like
334
409
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
341
416
        # if a "revid" is actually a dotted revno, convert it to a revid
342
417
        if revid is None:
343
418
            return revid
 
419
        if revid == 'head:':
 
420
            return self._last_revid
344
421
        if self.revno_re.match(revid):
345
422
            revid = self._revno_revid[revid]
346
423
        return revid
347
 
    
 
424
 
348
425
    @with_branch_lock
349
426
    def get_file_view(self, revid, file_id):
350
427
        """
351
 
        Given an optional revid and optional path, return a (revlist, revid)
352
 
        for navigation through the current scope: from the revid (or the
353
 
        latest revision) back to the original revision.
354
 
        
 
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
 
355
432
        If file_id is None, the entire revision history is the list scope.
356
 
        If revid is None, the latest revision is used.
357
433
        """
358
434
        if revid is None:
359
435
            revid = self._last_revid
360
436
        if file_id is not None:
361
 
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
362
 
            inv = self._branch.repository.get_revision_inventory(revid)
 
437
            # since revid is 'start_revid', possibly should start the path
 
438
            # tracing from revid... FIXME
363
439
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
364
440
            revlist = list(self.get_revids_from(revlist, revid))
365
441
        else:
366
442
            revlist = list(self.get_revids_from(None, revid))
367
 
        if revid is None:
368
 
            revid = revlist[0]
369
 
        return revlist, revid
370
 
    
 
443
        return revlist
 
444
 
371
445
    @with_branch_lock
372
446
    def get_view(self, revid, start_revid, file_id, query=None):
373
447
        """
374
448
        use the URL parameters (revid, start_revid, file_id, and query) to
375
449
        determine the revision list we're viewing (start_revid, file_id, query)
376
450
        and where we are in it (revid).
377
 
        
 
451
 
378
452
        if a query is given, we're viewing query results.
379
453
        if a file_id is given, we're viewing revisions for a specific file.
380
454
        if a start_revid is given, we're viewing the branch from a
381
455
            specific revision up the tree.
382
456
        (these may be combined to view revisions for a specific file, from
383
457
            a specific revision, with a specific search query.)
384
 
            
 
458
 
385
459
        returns a new (revid, start_revid, revid_list, scan_list) where:
386
 
        
 
460
 
387
461
            - revid: current position within the view
388
462
            - start_revid: starting revision of this view
389
463
            - revid_list: list of revision ids for this view
390
 
        
 
464
 
391
465
        file_id and query are never changed so aren't returned, but they may
392
466
        contain vital context for future url navigation.
393
467
        """
 
468
        if start_revid is None:
 
469
            start_revid = self._last_revid
 
470
 
394
471
        if query is None:
395
 
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
472
            revid_list = self.get_file_view(start_revid, file_id)
396
473
            if revid is None:
397
474
                revid = start_revid
398
475
            if revid not in revid_list:
399
476
                # if the given revid is not in the revlist, use a revlist that
400
477
                # starts at the given revid.
401
 
                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
402
480
            return revid, start_revid, revid_list
403
 
        
 
481
 
404
482
        # potentially limit the search
405
 
        if (start_revid is not None) or (file_id is not None):
406
 
            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)
407
485
        else:
408
486
            revid_list = None
409
487
 
428
506
        if (len(path) > 0) and not path.startswith('/'):
429
507
            path = '/' + path
430
508
        return path
431
 
    
432
 
    def get_where_merged(self, revid):
433
 
        try:
434
 
            return self._where_merged[revid]
435
 
        except:
436
 
            return []
437
 
    
 
509
 
 
510
    @with_branch_lock
 
511
    def get_file_id(self, revid, path):
 
512
        if (len(path) > 0) and not path.startswith('/'):
 
513
            path = '/' + path
 
514
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
 
515
 
 
516
 
438
517
    def get_merge_point_list(self, revid):
439
518
        """
440
519
        Return the list of revids that have merged this node.
441
520
        """
442
 
        if revid in self._history:
 
521
        if '.' not in self.get_revno(revid):
443
522
            return []
444
 
        
 
523
 
445
524
        merge_point = []
446
525
        while True:
447
 
            children = self.get_where_merged(revid)
 
526
            children = self._where_merged.get(revid, [])
448
527
            nexts = []
449
528
            for child in children:
450
529
                child_parents = self._revision_graph[child]
464
543
                merge_point.extend(merge_point_next)
465
544
 
466
545
            revid = nexts[0]
467
 
            
 
546
 
468
547
    def simplify_merge_point_list(self, revids):
469
548
        """if a revision is already merged, don't show further merge points"""
470
549
        d = {}
496
575
        p_changes = self.get_changes(list(fetch_set))
497
576
        p_change_dict = dict([(c.revid, c) for c in p_changes])
498
577
        for change in changes:
 
578
            # arch-converted branches may not have merged branch info :(
499
579
            for p in change.parents:
500
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
580
                if p.revid in p_change_dict:
 
581
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
582
                else:
 
583
                    p.branch_nick = '(missing)'
501
584
            for p in change.merge_points:
502
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
503
 
    
 
585
                if p.revid in p_change_dict:
 
586
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
587
                else:
 
588
                    p.branch_nick = '(missing)'
 
589
 
504
590
    @with_branch_lock
505
 
    def get_changes(self, revid_list, get_diffs=False):
 
591
    def get_changes(self, revid_list):
506
592
        if self._change_cache is None:
507
 
            changes = self.get_changes_uncached(revid_list, get_diffs)
 
593
            changes = self.get_changes_uncached(revid_list)
508
594
        else:
509
 
            changes = self._change_cache.get_changes(revid_list, get_diffs)
510
 
        if changes is None:
 
595
            changes = self._change_cache.get_changes(revid_list)
 
596
        if len(changes) == 0:
511
597
            return changes
512
 
        
 
598
 
513
599
        # some data needs to be recalculated each time, because it may
514
600
        # change as new revisions are added.
515
 
        for i in xrange(len(revid_list)):
516
 
            revid = revid_list[i]
517
 
            change = changes[i]
518
 
            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))
519
603
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
520
 
        
 
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
 
521
611
        return changes
522
612
 
523
613
    # alright, let's profile this sucka.
528
618
        stats.sort()
529
619
        stats.freeze()
530
620
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
 
621
        self.log.info('lsprof complete!')
531
622
        return ret
532
623
 
533
 
    @with_branch_lock
534
 
    @with_bzrlib_read_lock
535
 
    def get_changes_uncached(self, revid_list, get_diffs=False):
536
 
        try:
537
 
            rev_list = self._branch.repository.get_revisions(revid_list)
538
 
        except (KeyError, bzrlib.errors.NoSuchRevision):
539
 
            return None
540
 
        
541
 
        delta_list = self._branch.repository.get_deltas_for_revisions(rev_list)
542
 
        combined_list = zip(rev_list, delta_list)
543
 
        
544
 
        tree_map = {}
545
 
        if get_diffs:
546
 
            # lookup the trees for each revision, so we can calculate diffs
547
 
            lookup_set = set()
548
 
            for rev in rev_list:
549
 
                lookup_set.add(rev.revision_id)
550
 
                if len(rev.parent_ids) > 0:
551
 
                    lookup_set.add(rev.parent_ids[0])
552
 
            tree_map = dict((t.get_revision_id(), t) for t in self._branch.repository.revision_trees(lookup_set))
553
 
            # also the root tree, in case we hit the origin:
554
 
            tree_map[None] = self._branch.repository.revision_tree(None)
555
 
        
556
 
        entries = []
557
 
        for rev, delta in combined_list:
558
 
            commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
559
 
            
560
 
            parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
561
 
    
562
 
            if len(parents) == 0:
563
 
                left_parent = None
 
624
    def _get_deltas_for_revisions_with_trees(self, entries):
 
625
        """Produce a generator of revision deltas.
 
626
 
 
627
        Note that the input is a sequence of REVISIONS, not revision_ids.
 
628
        Trees will be held in memory until the generator exits.
 
629
        Each delta is relative to the revision's lefthand predecessor.
 
630
        """
 
631
        required_trees = set()
 
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
 
636
                     t in self._branch.repository.revision_trees(required_trees))
 
637
        ret = []
 
638
        self._branch.repository.lock_read()
 
639
        try:
 
640
            for entry in entries:
 
641
                if not entry.parents:
 
642
                    old_tree = self._branch.repository.revision_tree(
 
643
                        bzrlib.revision.NULL_REVISION)
 
644
                else:
 
645
                    old_tree = trees[entry.parents[0].revid]
 
646
                tree = trees[entry.revid]
 
647
                ret.append(tree.changes_from(old_tree))
 
648
            return ret
 
649
        finally:
 
650
            self._branch.repository.unlock()
 
651
 
 
652
    def entry_from_revision(self, revision):
 
653
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
 
654
 
 
655
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
 
656
 
 
657
        message, short_message = clean_message(revision.message)
 
658
 
 
659
        entry = {
 
660
            'revid': revision.revision_id,
 
661
            'date': commit_time,
 
662
            'author': revision.committer,
 
663
            'branch_nick': revision.properties.get('branch-nick', None),
 
664
            'short_comment': short_message,
 
665
            'comment': revision.message,
 
666
            'comment_clean': [util.html_clean(s) for s in message],
 
667
            'parents': parents,
 
668
        }
 
669
        return util.Container(entry)
 
670
 
 
671
    @with_branch_lock
 
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
564
717
            else:
565
 
                left_parent = rev.parent_ids[0]
566
 
            
567
 
            message = rev.message.splitlines()
568
 
            if len(message) == 1:
569
 
                # robey-style 1-line long message
570
 
                message = textwrap.wrap(message[0])
571
 
            
572
 
            # make short form of commit message
573
 
            short_message = message[0]
574
 
            if len(short_message) > 60:
575
 
                short_message = short_message[:60] + '...'
576
 
    
577
 
            old_tree, new_tree = None, None
578
 
            if get_diffs:
579
 
                new_tree = tree_map[rev.revision_id]
580
 
                old_tree = tree_map[left_parent]
581
 
 
582
 
            entry = {
583
 
                'revid': rev.revision_id,
584
 
                'revno': self.get_revno(rev.revision_id),
585
 
                'date': commit_time,
586
 
                'author': rev.committer,
587
 
                'branch_nick': rev.properties.get('branch-nick', None),
588
 
                'short_comment': short_message,
589
 
                'comment': rev.message,
590
 
                'comment_clean': [util.html_clean(s) for s in message],
591
 
                'parents': parents,
592
 
                'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
593
 
            }
594
 
            entries.append(util.Container(entry))
595
 
        
596
 
        return entries
 
718
                compare_revid = 'null:'
 
719
 
 
720
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
 
721
        rev_tree2 = self._branch.repository.revision_tree(revid)
 
722
        delta = rev_tree2.changes_from(rev_tree1)
 
723
 
 
724
        entry.changes = self.parse_delta(delta)
 
725
 
 
726
        entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
727
 
 
728
        return entry
597
729
 
598
730
    @with_branch_lock
599
731
    def get_file(self, file_id, revid):
600
 
        "returns (filename, data)"
601
 
        inv_entry = self.get_inventory(revid)[file_id]
 
732
        "returns (path, filename, data)"
 
733
        inv = self.get_inventory(revid)
 
734
        inv_entry = inv[file_id]
602
735
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
603
 
        return inv_entry.name, rev_tree.get_file_text(file_id)
604
 
    
605
 
    @with_branch_lock
606
 
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
 
736
        path = inv.id2path(file_id)
 
737
        if not path.startswith('/'):
 
738
            path = '/' + path
 
739
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
 
740
 
 
741
    def _parse_diffs(self, old_tree, new_tree, delta):
607
742
        """
608
 
        Return a nested data structure containing the changes in a delta::
609
 
        
610
 
            added: list((filename, file_id)),
611
 
            renamed: list((old_filename, new_filename, file_id)),
612
 
            deleted: list((filename, file_id)),
613
 
            modified: list(
 
743
        Return a list of processed diffs, in the format::
 
744
 
 
745
            list(
614
746
                filename: str,
615
747
                file_id: str,
616
748
                chunks: list(
622
754
                    ),
623
755
                ),
624
756
            )
625
 
        
626
 
        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
            )
627
834
        """
628
835
        added = []
629
836
        modified = []
630
837
        renamed = []
631
838
        removed = []
632
 
        
633
 
        def rich_filename(path, kind):
634
 
            if kind == 'directory':
635
 
                path += '/'
636
 
            if kind == 'symlink':
637
 
                path += '@'
638
 
            return path
639
 
        
640
 
        def process_diff(diff):
641
 
            chunks = []
642
 
            chunk = None
643
 
            for line in diff.splitlines():
644
 
                if len(line) == 0:
645
 
                    continue
646
 
                if line.startswith('+++ ') or line.startswith('--- '):
647
 
                    continue
648
 
                if line.startswith('@@ '):
649
 
                    # new chunk
650
 
                    if chunk is not None:
651
 
                        chunks.append(chunk)
652
 
                    chunk = util.Container()
653
 
                    chunk.diff = []
654
 
                    lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
655
 
                    old_lineno = lines[0]
656
 
                    new_lineno = lines[1]
657
 
                elif line.startswith(' '):
658
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
659
 
                                                     type='context', line=util.html_clean(line[1:])))
660
 
                    old_lineno += 1
661
 
                    new_lineno += 1
662
 
                elif line.startswith('+'):
663
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
664
 
                                                     type='insert', line=util.html_clean(line[1:])))
665
 
                    new_lineno += 1
666
 
                elif line.startswith('-'):
667
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
668
 
                                                     type='delete', line=util.html_clean(line[1:])))
669
 
                    old_lineno += 1
670
 
                else:
671
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
672
 
                                                     type='unknown', line=util.html_clean(repr(line))))
673
 
            if chunk is not None:
674
 
                chunks.append(chunk)
675
 
            return chunks
676
 
                    
677
 
        def handle_modify(old_path, new_path, fid, kind):
678
 
            if not get_diffs:
679
 
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
680
 
                return
681
 
            old_lines = old_tree.get_file_lines(fid)
682
 
            new_lines = new_tree.get_file_lines(fid)
683
 
            buffer = StringIO()
684
 
            bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
685
 
            diff = buffer.getvalue()
686
 
            modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
687
839
 
688
840
        for path, fid, kind in delta.added:
689
841
            added.append((rich_filename(path, kind), fid))
690
 
        
 
842
 
691
843
        for path, fid, kind, text_modified, meta_modified in delta.modified:
692
 
            handle_modify(path, path, fid, kind)
693
 
        
694
 
        for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
695
 
            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))
696
848
            if meta_modified or text_modified:
697
 
                handle_modify(oldpath, newpath, fid, kind)
698
 
        
 
849
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
850
 
699
851
        for path, fid, kind in delta.removed:
700
852
            removed.append((rich_filename(path, kind), fid))
701
 
        
 
853
 
702
854
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
703
855
 
 
856
    @staticmethod
 
857
    def add_side_by_side(changes):
 
858
        # FIXME: this is a rotten API.
 
859
        for change in changes:
 
860
            for m in change.changes.modified:
 
861
                m.sbs_chunks = _make_side_by_side(m.chunks)
 
862
 
704
863
    @with_branch_lock
705
 
    def get_filelist(self, inv, path, sort_type=None):
 
864
    def get_filelist(self, inv, file_id, sort_type=None):
706
865
        """
707
866
        return the list of all files (and their attributes) within a given
708
867
        path subtree.
709
868
        """
710
 
        while path.endswith('/'):
711
 
            path = path[:-1]
712
 
        if path.startswith('/'):
713
 
            path = path[1:]
714
 
        
715
 
        entries = inv.entries()
716
 
        
717
 
        fetch_set = set()
718
 
        for filepath, entry in entries:
719
 
            fetch_set.add(entry.revision)
720
 
        change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
721
 
        
 
869
 
 
870
        dir_ie = inv[file_id]
 
871
        path = inv.id2path(file_id)
722
872
        file_list = []
723
 
        for filepath, entry in entries:
724
 
            if posixpath.dirname(filepath) != path:
725
 
                continue
726
 
            filename = posixpath.basename(filepath)
727
 
            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():
728
884
            pathname = filename
729
885
            if entry.kind == 'directory':
730
886
                pathname += '/'
731
 
            
732
 
            # last change:
 
887
 
733
888
            revid = entry.revision
734
 
            change = change_dict[revid]
735
 
            
736
 
            file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
737
 
                                  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])
738
894
            file_list.append(file)
739
 
        
740
 
        if sort_type == 'filename':
 
895
 
 
896
        if sort_type == 'filename' or sort_type is None:
741
897
            file_list.sort(key=lambda x: x.filename)
742
898
        elif sort_type == 'size':
743
899
            file_list.sort(key=lambda x: x.size)
744
900
        elif sort_type == 'date':
745
901
            file_list.sort(key=lambda x: x.change.date)
746
 
        
 
902
 
747
903
        parity = 0
748
904
        for file in file_list:
749
905
            file.parity = parity
752
908
        return file_list
753
909
 
754
910
 
755
 
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
 
911
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
756
912
 
757
913
    @with_branch_lock
758
914
    def annotate_file(self, file_id, revid):
759
915
        z = time.time()
760
916
        lineno = 1
761
917
        parity = 0
762
 
        
 
918
 
763
919
        file_revid = self.get_inventory(revid)[file_id].revision
764
920
        oldvalues = None
765
 
        
 
921
 
766
922
        # because we cache revision metadata ourselves, it's actually much
767
923
        # faster to call 'annotate_iter' on the weave directly than it is to
768
924
        # ask bzrlib to annotate for us.
769
925
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
770
 
        
 
926
 
771
927
        revid_set = set()
772
928
        for line_revid, text in w.annotate_iter(file_revid):
773
929
            revid_set.add(line_revid)
774
930
            if self._BADCHARS_RE.match(text):
775
931
                # bail out; this isn't displayable text
776
932
                yield util.Container(parity=0, lineno=1, status='same',
777
 
                                     text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
 
933
                                     text='(This is a binary file.)',
778
934
                                     change=util.Container())
779
935
                return
780
936
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
781
 
        
 
937
 
782
938
        last_line_revid = None
783
939
        for line_revid, text in w.annotate_iter(file_revid):
784
940
            if line_revid == last_line_revid:
792
948
                trunc_revno = change.revno
793
949
                if len(trunc_revno) > 10:
794
950
                    trunc_revno = trunc_revno[:9] + '...'
795
 
                
 
951
 
796
952
            yield util.Container(parity=parity, lineno=lineno, status=status,
797
 
                                 change=change, text=util.html_clean(text))
 
953
                                 change=change, text=util.fixed_width(text))
798
954
            lineno += 1
799
 
        
 
955
 
800
956
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
957
 
 
958
    @with_branch_lock
 
959
    def get_bundle(self, revid, compare_revid=None):
 
960
        if compare_revid is None:
 
961
            parents = self._revision_graph[revid]
 
962
            if len(parents) > 0:
 
963
                compare_revid = parents[0]
 
964
            else:
 
965
                compare_revid = None
 
966
        s = StringIO()
 
967
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
 
968
        return s.getvalue()
 
969