~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Robey Pointer
  • Date: 2006-12-24 07:04:28 UTC
  • Revision ID: robey@lag.net-20061224070428-u2tbimufx0m1v16t
add the actual 1.0 release

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
3
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
 
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
 
5
# Copyright (C) 2005  Matt Mackall <mpm@selenic.com>
4
6
#
5
7
# This program is free software; you can redistribute it and/or modify
6
8
# it under the terms of the GNU General Public License as published by
17
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
20
#
19
21
 
 
22
#
 
23
# This file (and many of the web templates) contains work based on the
 
24
# "bazaar-webserve" project by Goffredo Baroncelli, which is in turn based
 
25
# on "hgweb" by Jake Edge and Matt Mackall.
 
26
#
 
27
 
 
28
 
 
29
import bisect
20
30
import cgi
21
31
import datetime
22
32
import logging
31
41
from StringIO import StringIO
32
42
 
33
43
from loggerhead import util
34
 
extra_path = util.get_config().get('bzrpath', None)
35
 
if extra_path:
36
 
    sys.path.insert(0, extra_path)
 
44
from loggerhead.util import decorator
37
45
 
38
46
import bzrlib
39
47
import bzrlib.annotate
40
48
import bzrlib.branch
 
49
import bzrlib.bundle.serializer
41
50
import bzrlib.diff
42
51
import bzrlib.errors
43
52
import bzrlib.progress
46
55
import bzrlib.ui
47
56
 
48
57
 
49
 
log = logging.getLogger("loggerhead.controllers")
50
 
 
51
 
 
52
 
# cache lock binds tighter than branch lock
53
 
def with_cache_lock(unbound):
54
 
    def cache_locked(self, *args, **kw):
55
 
        self._cache_lock.acquire()
56
 
        try:
57
 
            return unbound(self, *args, **kw)
58
 
        finally:
59
 
            self._cache_lock.release()
60
 
    cache_locked.__doc__ = unbound.__doc__
61
 
    cache_locked.__name__ = unbound.__name__
62
 
    return cache_locked
63
 
 
64
 
 
65
 
def with_branch_lock(unbound):
66
 
    def branch_locked(self, *args, **kw):
67
 
        self._lock.acquire()
68
 
        try:
69
 
            return unbound(self, *args, **kw)
70
 
        finally:
71
 
            self._lock.release()
72
 
    branch_locked.__doc__ = unbound.__doc__
73
 
    branch_locked.__name__ = unbound.__name__
74
 
    return branch_locked
 
58
with_branch_lock = util.with_lock('_lock', 'branch')
 
59
 
 
60
@decorator
 
61
def with_bzrlib_read_lock(unbound):
 
62
    def bzrlib_read_locked(self, *args, **kw):
 
63
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
 
64
        self._branch.repository.lock_read()
 
65
        try:
 
66
            return unbound(self, *args, **kw)
 
67
        finally:
 
68
            self._branch.repository.unlock()
 
69
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
 
70
    return bzrlib_read_locked
75
71
 
76
72
 
77
73
# bzrlib's UIFactory is not thread-safe
86
82
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
87
83
 
88
84
 
 
85
# from bzrlib
 
86
class _RevListToTimestamps(object):
 
87
    """This takes a list of revisions, and allows you to bisect by date"""
 
88
 
 
89
    __slots__ = ['revid_list', 'repository']
 
90
 
 
91
    def __init__(self, revid_list, repository):
 
92
        self.revid_list = revid_list
 
93
        self.repository = repository
 
94
 
 
95
    def __getitem__(self, index):
 
96
        """Get the date of the index'd item"""
 
97
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
 
98
 
 
99
    def __len__(self):
 
100
        return len(self.revid_list)
 
101
 
 
102
 
89
103
class History (object):
90
104
    
91
105
    def __init__(self):
92
106
        self._change_cache = None
93
 
        self._cache_lock = threading.Lock()
 
107
        self._index = None
94
108
        self._lock = threading.RLock()
95
109
    
96
 
    def __del__(self):
97
 
        if self._change_cache is not None:
98
 
            self._change_cache.close()
99
 
            self._change_cache_diffs.close()
100
 
            self._change_cache = None
101
 
            self._change_cache_diffs = None
102
 
 
103
110
    @classmethod
104
 
    def from_branch(cls, branch):
 
111
    def from_branch(cls, branch, name=None):
105
112
        z = time.time()
106
113
        self = cls()
107
114
        self._branch = branch
108
115
        self._history = branch.revision_history()
109
 
        self._revision_graph = branch.repository.get_revision_graph()
110
116
        self._last_revid = self._history[-1]
 
117
        self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
 
118
        
 
119
        if name is None:
 
120
            name = self._branch.nick
 
121
        self._name = name
 
122
        self.log = logging.getLogger('loggerhead.%s' % (name,))
111
123
        
112
124
        self._full_history = []
113
125
        self._revision_info = {}
130
142
            for parent in self._revision_graph[revid]:
131
143
                self._where_merged.setdefault(parent, set()).add(revid)
132
144
 
133
 
        log.info('built revision graph cache: %r secs' % (time.time() - z,))
 
145
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
134
146
        return self
135
147
    
136
148
    @classmethod
137
 
    def from_folder(cls, path):
 
149
    def from_folder(cls, path, name=None):
138
150
        b = bzrlib.branch.Branch.open(path)
139
 
        return cls.from_branch(b)
 
151
        return cls.from_branch(b, name)
140
152
 
141
153
    @with_branch_lock
142
154
    def out_of_date(self):
144
156
            return True
145
157
        return False
146
158
 
147
 
    @with_cache_lock
148
 
    def use_cache(self, path):
149
 
        if not os.path.exists(path):
150
 
            os.mkdir(path)
151
 
        # keep a separate cache for the diffs, because they're very time-consuming to fetch.
152
 
        cachefile = os.path.join(path, 'changes')
153
 
        cachefile_diffs = os.path.join(path, 'changes-diffs')
154
 
        
155
 
        # why can't shelve allow 'cw'?
156
 
        if not os.path.exists(cachefile):
157
 
            self._change_cache = shelve.open(cachefile, 'c', protocol=2)
158
 
        else:
159
 
            self._change_cache = shelve.open(cachefile, 'w', protocol=2)
160
 
        if not os.path.exists(cachefile_diffs):
161
 
            self._change_cache_diffs = shelve.open(cachefile_diffs, 'c', protocol=2)
162
 
        else:
163
 
            self._change_cache_diffs = shelve.open(cachefile_diffs, 'w', protocol=2)
164
 
            
165
 
        # once we process a change (revision), it should be the same forever.
166
 
        log.info('Using change cache %s; %d, %d entries.' % (path, len(self._change_cache), len(self._change_cache_diffs)))
167
 
        self._change_cache_filename = cachefile
168
 
        self._change_cache_diffs_filename = cachefile_diffs
169
 
 
170
 
    @with_cache_lock
171
 
    def dont_use_cache(self):
172
 
        # called when a new history object needs to be created.  we can't use
173
 
        # the cache files anymore; they belong to the new history object.
174
 
        if self._change_cache is None:
175
 
            return
176
 
        self._change_cache.close()
177
 
        self._change_cache_diffs.close()
178
 
        self._change_cache = None
179
 
        self._change_cache_diffs = None
180
 
 
181
 
    @with_cache_lock
 
159
    def use_cache(self, cache):
 
160
        self._change_cache = cache
 
161
    
 
162
    def use_search_index(self, index):
 
163
        self._index = index
 
164
 
 
165
    @with_branch_lock
 
166
    def detach(self):
 
167
        # called when a new history object needs to be created, because the
 
168
        # branch history has changed.  we need to immediately close and stop
 
169
        # using our caches, because a new history object will be created to
 
170
        # replace us, using the same cache files.
 
171
        if self._change_cache is not None:
 
172
            self._change_cache.close()
 
173
            self._change_cache = None
 
174
        if self._index is not None:
 
175
            self._index.close()
 
176
            self._index = None
 
177
 
182
178
    def flush_cache(self):
183
179
        if self._change_cache is None:
184
180
            return
185
 
        self._change_cache.sync()
186
 
        self._change_cache_diffs.sync()
 
181
        self._change_cache.flush()
 
182
    
 
183
    def check_rebuild(self):
 
184
        if self._change_cache is not None:
 
185
            self._change_cache.check_rebuild()
 
186
        if self._index is not None:
 
187
            self._index.check_rebuild()
187
188
    
188
189
    last_revid = property(lambda self: self._last_revid, None, None)
189
190
    
233
234
            if len(parents) == 0:
234
235
                return
235
236
            revid = parents[0]
236
 
 
 
237
    
237
238
    @with_branch_lock
238
239
    def get_short_revision_history_by_fileid(self, file_id):
239
240
        # wow.  is this really the only way we can get this list?  by
245
246
        revids = [r for r in self._full_history if r in w_revids]
246
247
        return revids
247
248
 
 
249
    @with_branch_lock
 
250
    def get_revision_history_since(self, revid_list, date):
 
251
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
 
252
        # so start at midnight on 02-sep.
 
253
        date = date + datetime.timedelta(days=1)
 
254
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
 
255
        revid_list.reverse()
 
256
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
 
257
        if index == 0:
 
258
            return []
 
259
        revid_list.reverse()
 
260
        index = -index
 
261
        return revid_list[index:]
 
262
    
 
263
    @with_branch_lock
 
264
    def get_revision_history_matching(self, revid_list, text):
 
265
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
266
        z = time.time()
 
267
        # this is going to be painfully slow. :(
 
268
        out = []
 
269
        text = text.lower()
 
270
        for revid in revid_list:
 
271
            change = self.get_changes([ revid ])[0]
 
272
            if text in change.comment.lower():
 
273
                out.append(revid)
 
274
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
 
275
        return out
 
276
 
 
277
    def get_revision_history_matching_indexed(self, revid_list, text):
 
278
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
279
        z = time.time()
 
280
        if self._index is None:
 
281
            return self.get_revision_history_matching(revid_list, text)
 
282
        out = self._index.find(text, revid_list)
 
283
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
 
284
        # put them in some coherent order :)
 
285
        out = [r for r in self._full_history if r in out]
 
286
        return out
 
287
    
 
288
    @with_branch_lock
 
289
    def get_search_revid_list(self, query, revid_list):
 
290
        """
 
291
        given a "quick-search" query, try a few obvious possible meanings:
 
292
        
 
293
            - revision id or # ("128.1.3")
 
294
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
 
295
            - comment text as a fallback
 
296
 
 
297
        and return a revid list that matches.
 
298
        """
 
299
        # FIXME: there is some silliness in this action.  we have to look up
 
300
        # all the relevant changes (time-consuming) only to return a list of
 
301
        # revids which will be used to fetch a set of changes again.
 
302
        
 
303
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
 
304
        revid = self.fix_revid(query)
 
305
        if revid is not None:
 
306
            changes = self.get_changes([ revid ])
 
307
            if (changes is not None) and (len(changes) > 0):
 
308
                return [ revid ]
 
309
        
 
310
        date = None
 
311
        m = self.us_date_re.match(query)
 
312
        if m is not None:
 
313
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
 
314
        else:
 
315
            m = self.earth_date_re.match(query)
 
316
            if m is not None:
 
317
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
 
318
            else:
 
319
                m = self.iso_date_re.match(query)
 
320
                if m is not None:
 
321
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
 
322
        if date is not None:
 
323
            if revid_list is None:
 
324
                # if no limit to the query was given, search only the direct-parent path.
 
325
                revid_list = list(self.get_revids_from(None, self._last_revid))
 
326
            return self.get_revision_history_since(revid_list, date)
 
327
        
 
328
        # check comment fields.
 
329
        if revid_list is None:
 
330
            revid_list = self._full_history
 
331
        return self.get_revision_history_matching_indexed(revid_list, query)
 
332
    
248
333
    revno_re = re.compile(r'^[\d\.]+$')
 
334
    # the date regex are without a final '$' so that queries like
 
335
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
 
336
    # them 90% of what they want instead of nothing at all.)
 
337
    us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
 
338
    earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
 
339
    iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
249
340
 
250
341
    def fix_revid(self, revid):
251
342
        # if a "revid" is actually a dotted revno, convert it to a revid
256
347
        return revid
257
348
    
258
349
    @with_branch_lock
259
 
    def get_navigation(self, revid, path):
 
350
    def get_file_view(self, revid, file_id):
260
351
        """
261
352
        Given an optional revid and optional path, return a (revlist, revid)
262
353
        for navigation through the current scope: from the revid (or the
263
354
        latest revision) back to the original revision.
264
355
        
265
 
        If path is None, the entire revision history is the list scope.
 
356
        If file_id is None, the entire revision history is the list scope.
266
357
        If revid is None, the latest revision is used.
267
358
        """
268
359
        if revid is None:
269
360
            revid = self._last_revid
270
 
        if path is not None:
 
361
        if file_id is not None:
271
362
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
272
363
            inv = self._branch.repository.get_revision_inventory(revid)
273
 
            revlist = list(self.get_short_revision_history_by_fileid(inv.path2id(path)))
 
364
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
274
365
            revlist = list(self.get_revids_from(revlist, revid))
275
366
        else:
276
367
            revlist = list(self.get_revids_from(None, revid))
277
368
        if revid is None:
278
369
            revid = revlist[0]
279
370
        return revlist, revid
 
371
    
 
372
    @with_branch_lock
 
373
    def get_view(self, revid, start_revid, file_id, query=None):
 
374
        """
 
375
        use the URL parameters (revid, start_revid, file_id, and query) to
 
376
        determine the revision list we're viewing (start_revid, file_id, query)
 
377
        and where we are in it (revid).
 
378
        
 
379
        if a query is given, we're viewing query results.
 
380
        if a file_id is given, we're viewing revisions for a specific file.
 
381
        if a start_revid is given, we're viewing the branch from a
 
382
            specific revision up the tree.
 
383
        (these may be combined to view revisions for a specific file, from
 
384
            a specific revision, with a specific search query.)
 
385
            
 
386
        returns a new (revid, start_revid, revid_list, scan_list) where:
 
387
        
 
388
            - revid: current position within the view
 
389
            - start_revid: starting revision of this view
 
390
            - revid_list: list of revision ids for this view
 
391
        
 
392
        file_id and query are never changed so aren't returned, but they may
 
393
        contain vital context for future url navigation.
 
394
        """
 
395
        if query is None:
 
396
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
397
            if revid is None:
 
398
                revid = start_revid
 
399
            if revid not in revid_list:
 
400
                # if the given revid is not in the revlist, use a revlist that
 
401
                # starts at the given revid.
 
402
                revid_list, start_revid = self.get_file_view(revid, file_id)
 
403
            return revid, start_revid, revid_list
 
404
        
 
405
        # potentially limit the search
 
406
        if (start_revid is not None) or (file_id is not None):
 
407
            revid_list, start_revid = self.get_file_view(start_revid, file_id)
 
408
        else:
 
409
            revid_list = None
 
410
 
 
411
        revid_list = self.get_search_revid_list(query, revid_list)
 
412
        if len(revid_list) > 0:
 
413
            if revid not in revid_list:
 
414
                revid = revid_list[0]
 
415
            return revid, start_revid, revid_list
 
416
        else:
 
417
            # no results
 
418
            return None, None, []
280
419
 
281
420
    @with_branch_lock
282
421
    def get_inventory(self, revid):
283
422
        return self._branch.repository.get_revision_inventory(revid)
284
423
 
 
424
    @with_branch_lock
 
425
    def get_path(self, revid, file_id):
 
426
        if (file_id is None) or (file_id == ''):
 
427
            return ''
 
428
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
 
429
        if (len(path) > 0) and not path.startswith('/'):
 
430
            path = '/' + path
 
431
        return path
 
432
    
285
433
    def get_where_merged(self, revid):
286
434
        try:
287
435
            return self._where_merged[revid]
334
482
                d[revnos] = ( revnolast, revid )
335
483
 
336
484
        return [ d[revnos][1] for revnos in d.keys() ]
337
 
            
338
 
    def get_changelist(self, revid_list):
339
 
        for revid in revid_list:
340
 
            yield self.get_change(revid)
 
485
 
 
486
    def get_branch_nicks(self, changes):
 
487
        """
 
488
        given a list of changes from L{get_changes}, fill in the branch nicks
 
489
        on all parents and merge points.
 
490
        """
 
491
        fetch_set = set()
 
492
        for change in changes:
 
493
            for p in change.parents:
 
494
                fetch_set.add(p.revid)
 
495
            for p in change.merge_points:
 
496
                fetch_set.add(p.revid)
 
497
        p_changes = self.get_changes(list(fetch_set))
 
498
        p_change_dict = dict([(c.revid, c) for c in p_changes])
 
499
        for change in changes:
 
500
            for p in change.parents:
 
501
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
502
            for p in change.merge_points:
 
503
                p.branch_nick = p_change_dict[p.revid].branch_nick
341
504
    
342
505
    @with_branch_lock
343
 
    def get_change(self, revid, get_diffs=False):
 
506
    def get_changes(self, revid_list, get_diffs=False):
344
507
        if self._change_cache is None:
345
 
            return self._get_change(revid, get_diffs)
346
 
 
347
 
        # if the revid is in unicode, use the utf-8 encoding as the key
348
 
        srevid = revid
349
 
        if isinstance(revid, unicode):
350
 
            srevid = revid.encode('utf-8')
351
 
        return self._get_change_from_cache(revid, srevid, get_diffs)
352
 
 
353
 
    @with_cache_lock
354
 
    def _get_change_from_cache(self, revid, srevid, get_diffs):
355
 
        if get_diffs:
356
 
            cache = self._change_cache_diffs
357
 
        else:
358
 
            cache = self._change_cache
359
 
            
360
 
        if srevid in cache:
361
 
            c = cache[srevid]
362
 
        else:
363
 
            if get_diffs and (srevid in self._change_cache):
364
 
                # salvage the non-diff entry for a jump-start
365
 
                c = self._change_cache[srevid]
366
 
                if len(c.parents) == 0:
367
 
                    left_parent = None
368
 
                else:
369
 
                    left_parent = c.parents[0].revid
370
 
                c.changes = self.diff_revisions(revid, left_parent, get_diffs=True)
371
 
                cache[srevid] = c
372
 
            else:
373
 
                #log.debug('Entry cache miss: %r' % (revid,))
374
 
                c = self._get_change(revid, get_diffs=get_diffs)
375
 
                cache[srevid] = c
376
 
            
 
508
            changes = self.get_changes_uncached(revid_list, get_diffs)
 
509
        else:
 
510
            changes = self._change_cache.get_changes(revid_list, get_diffs)
 
511
        if changes is None:
 
512
            return changes
 
513
        
377
514
        # some data needs to be recalculated each time, because it may
378
515
        # change as new revisions are added.
379
 
        merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
380
 
        c.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
 
516
        for i in xrange(len(revid_list)):
 
517
            revid = revid_list[i]
 
518
            change = changes[i]
 
519
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
 
520
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
381
521
        
382
 
        return c
383
 
    
 
522
        return changes
 
523
 
384
524
    # alright, let's profile this sucka.
385
 
    def _get_change_profiled(self, revid, get_diffs=False):
 
525
    def _get_changes_profiled(self, revid_list, get_diffs=False):
386
526
        from loggerhead.lsprof import profile
387
527
        import cPickle
388
 
        ret, stats = profile(self._get_change, revid, get_diffs)
 
528
        ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
389
529
        stats.sort()
390
530
        stats.freeze()
391
531
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
392
532
        return ret
393
533
 
394
 
    def _get_change(self, revid, get_diffs=False):
 
534
    @with_branch_lock
 
535
    @with_bzrlib_read_lock
 
536
    def get_changes_uncached(self, revid_list, get_diffs=False):
395
537
        try:
396
 
            rev = self._branch.repository.get_revision(revid)
 
538
            rev_list = self._branch.repository.get_revisions(revid_list)
397
539
        except (KeyError, bzrlib.errors.NoSuchRevision):
398
 
            # ghosted parent?
 
540
            return None
 
541
        
 
542
        delta_list = self._branch.repository.get_deltas_for_revisions(rev_list)
 
543
        combined_list = zip(rev_list, delta_list)
 
544
        
 
545
        tree_map = {}
 
546
        if get_diffs:
 
547
            # lookup the trees for each revision, so we can calculate diffs
 
548
            lookup_set = set()
 
549
            for rev in rev_list:
 
550
                lookup_set.add(rev.revision_id)
 
551
                if len(rev.parent_ids) > 0:
 
552
                    lookup_set.add(rev.parent_ids[0])
 
553
            tree_map = dict((t.get_revision_id(), t) for t in self._branch.repository.revision_trees(lookup_set))
 
554
            # also the root tree, in case we hit the origin:
 
555
            tree_map[None] = self._branch.repository.revision_tree(None)
 
556
        
 
557
        entries = []
 
558
        for rev, delta in combined_list:
 
559
            commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
 
560
            
 
561
            parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
 
562
    
 
563
            if len(parents) == 0:
 
564
                left_parent = None
 
565
            else:
 
566
                left_parent = rev.parent_ids[0]
 
567
            
 
568
            message = rev.message.splitlines()
 
569
            if len(message) == 1:
 
570
                # robey-style 1-line long message
 
571
                message = textwrap.wrap(message[0])
 
572
            
 
573
            # make short form of commit message
 
574
            short_message = message[0]
 
575
            if len(short_message) > 60:
 
576
                short_message = short_message[:60] + '...'
 
577
    
 
578
            old_tree, new_tree = None, None
 
579
            if get_diffs:
 
580
                new_tree = tree_map[rev.revision_id]
 
581
                old_tree = tree_map[left_parent]
 
582
 
399
583
            entry = {
400
 
                'revid': 'missing',
401
 
                'revno': '',
402
 
                'date': datetime.datetime.fromtimestamp(0),
403
 
                'author': 'missing',
404
 
                'branch_nick': None,
405
 
                'short_comment': 'missing',
406
 
                'comment': 'missing',
407
 
                'comment_clean': 'missing',
408
 
                'parents': [],
409
 
                'merge_points': [],
410
 
                'changes': [],
 
584
                'revid': rev.revision_id,
 
585
                'revno': self.get_revno(rev.revision_id),
 
586
                'date': commit_time,
 
587
                'author': rev.committer,
 
588
                'branch_nick': rev.properties.get('branch-nick', None),
 
589
                'short_comment': short_message,
 
590
                'comment': rev.message,
 
591
                'comment_clean': [util.html_clean(s) for s in message],
 
592
                'parents': parents,
 
593
                'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
411
594
            }
412
 
            log.error('ghost entry: %r' % (revid,))
413
 
            return util.Container(entry)
414
 
            
415
 
        commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
416
 
        
417
 
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
418
 
 
419
 
        if len(parents) == 0:
420
 
            left_parent = None
421
 
        else:
422
 
            left_parent = rev.parent_ids[0]
423
 
        
424
 
        message = rev.message.splitlines()
425
 
        if len(message) == 1:
426
 
            # robey-style 1-line long message
427
 
            message = textwrap.wrap(message[0])
428
 
        
429
 
        # make short form of commit message
430
 
        short_message = message[0]
431
 
        if len(short_message) > 60:
432
 
            short_message = short_message[:60] + '...'
433
 
 
434
 
        entry = {
435
 
            'revid': revid,
436
 
            'revno': self.get_revno(revid),
437
 
            'date': commit_time,
438
 
            'author': rev.committer,
439
 
            'branch_nick': rev.properties.get('branch-nick', None),
440
 
            'short_comment': short_message,
441
 
            'comment': rev.message,
442
 
            'comment_clean': [util.html_clean(s) for s in message],
443
 
            'parents': parents,
444
 
            'changes': self.diff_revisions(revid, left_parent, get_diffs=get_diffs),
445
 
        }
446
 
        return util.Container(entry)
447
 
    
448
 
    def scan_range(self, revlist, revid, pagesize=20):
449
 
        """
450
 
        yield a list of (label, title, revid) for a scan range through the full
451
 
        branch history, centered around the given revid.
452
 
        
453
 
        example: [ ('<<', 'Previous page', 'rrr'), ('-10', 'Forward 10', 'rrr'),
454
 
                   ('*', None, None), ('+10', 'Back 10', 'rrr'),
455
 
                   ('+30', 'Back 30', 'rrr'), ('>>', 'Next page', 'rrr') ]
456
 
        
457
 
        next/prev page are always using the pagesize.
458
 
        """
459
 
        count = len(revlist)
460
 
        pos = self.get_revid_sequence(revlist, revid)
461
 
 
462
 
        if pos > 0:
463
 
            yield (u'\xab', 'Previous page', revlist[max(0, pos - pagesize)])
464
 
        else:
465
 
            yield (u'\xab', None, None)
466
 
        
467
 
        offset_sign = -1
468
 
        for offset in util.scan_range(pos, count, pagesize):
469
 
            if (offset > 0) and (offset_sign < 0):
470
 
                offset_sign = 0
471
 
                # show current position
472
 
#                yield ('[%s]' % (self.get_revno(revlist[pos]),), None, None)
473
 
#                yield (u'\u2022', None, None)
474
 
                yield (u'\u00b7', None, None)
475
 
            if offset < 0:
476
 
                title = 'Back %d' % (-offset,)
477
 
            else:
478
 
                title = 'Forward %d' % (offset,)
479
 
            yield ('%+d' % (offset,), title, revlist[pos + offset])
480
 
        
481
 
        if pos < count - 1:
482
 
            yield (u'\xbb', 'Next page', revlist[min(count - 1, pos + pagesize)])
483
 
        else:
484
 
            yield (u'\xbb', None, None)
485
 
    
486
 
    @with_branch_lock
487
 
    def diff_revisions(self, revid, otherrevid, get_diffs=True):
488
 
        """
489
 
        Return a nested data structure containing the changes between two
490
 
        revisions::
491
 
        
492
 
            added: list(filename),
493
 
            renamed: list((old_filename, new_filename)),
494
 
            deleted: list(filename),
 
595
            entries.append(util.Container(entry))
 
596
        
 
597
        return entries
 
598
 
 
599
    @with_branch_lock
 
600
    def get_file(self, file_id, revid):
 
601
        "returns (filename, data)"
 
602
        inv_entry = self.get_inventory(revid)[file_id]
 
603
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
 
604
        return inv_entry.name, rev_tree.get_file_text(file_id)
 
605
    
 
606
    @with_branch_lock
 
607
    def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
 
608
        """
 
609
        Return a nested data structure containing the changes in a delta::
 
610
        
 
611
            added: list((filename, file_id)),
 
612
            renamed: list((old_filename, new_filename, file_id)),
 
613
            deleted: list((filename, file_id)),
495
614
            modified: list(
496
615
                filename: str,
 
616
                file_id: str,
497
617
                chunks: list(
498
618
                    diff: list(
499
619
                        old_lineno: int,
506
626
        
507
627
        if C{get_diffs} is false, the C{chunks} will be omitted.
508
628
        """
509
 
 
510
 
        new_tree = self._branch.repository.revision_tree(revid)
511
 
        old_tree = self._branch.repository.revision_tree(otherrevid)
512
 
        delta = new_tree.changes_from(old_tree)
513
 
        
514
629
        added = []
515
630
        modified = []
516
631
        renamed = []
523
638
                path += '@'
524
639
            return path
525
640
        
526
 
        def tree_lines(tree, fid):
527
 
            if not fid in tree:
528
 
                return []
529
 
            tree_file = bzrlib.textfile.text_file(tree.get_file(fid))
530
 
            return tree_file.readlines()
531
 
        
532
641
        def process_diff(diff):
533
642
            chunks = []
534
643
            chunk = None
568
677
                    
569
678
        def handle_modify(old_path, new_path, fid, kind):
570
679
            if not get_diffs:
571
 
                modified.append(util.Container(filename=rich_filename(new_path, kind)))
 
680
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
572
681
                return
573
 
            old_lines = tree_lines(old_tree, fid)
574
 
            new_lines = tree_lines(new_tree, fid)
 
682
            old_lines = old_tree.get_file_lines(fid)
 
683
            new_lines = new_tree.get_file_lines(fid)
575
684
            buffer = StringIO()
576
685
            bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
577
686
            diff = buffer.getvalue()
578
 
            modified.append(util.Container(filename=rich_filename(new_path, kind), chunks=process_diff(diff)))
 
687
            modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
579
688
 
580
689
        for path, fid, kind in delta.added:
581
 
            added.append(rich_filename(path, kind))
 
690
            added.append((rich_filename(path, kind), fid))
582
691
        
583
692
        for path, fid, kind, text_modified, meta_modified in delta.modified:
584
693
            handle_modify(path, path, fid, kind)
585
694
        
586
695
        for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
587
 
            renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind)))
 
696
            renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
588
697
            if meta_modified or text_modified:
589
698
                handle_modify(oldpath, newpath, fid, kind)
590
699
        
591
700
        for path, fid, kind in delta.removed:
592
 
            removed.append(rich_filename(path, kind))
 
701
            removed.append((rich_filename(path, kind), fid))
593
702
        
594
703
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
595
704
 
596
705
    @with_branch_lock
597
 
    def get_filelist(self, inv, path):
 
706
    def get_filelist(self, inv, path, sort_type=None):
598
707
        """
599
708
        return the list of all files (and their attributes) within a given
600
709
        path subtree.
603
712
            path = path[:-1]
604
713
        if path.startswith('/'):
605
714
            path = path[1:]
606
 
        parity = 0
607
 
        for filepath, entry in inv.entries():
 
715
        
 
716
        entries = inv.entries()
 
717
        
 
718
        fetch_set = set()
 
719
        for filepath, entry in entries:
 
720
            fetch_set.add(entry.revision)
 
721
        change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
 
722
        
 
723
        file_list = []
 
724
        for filepath, entry in entries:
608
725
            if posixpath.dirname(filepath) != path:
609
726
                continue
610
727
            filename = posixpath.basename(filepath)
615
732
            
616
733
            # last change:
617
734
            revid = entry.revision
618
 
            change = self.get_change(revid)
 
735
            change = change_dict[revid]
619
736
            
620
 
            yield util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
621
 
                                 pathname=pathname, revid=revid, change=change, parity=parity)
 
737
            file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
 
738
                                  pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
 
739
            file_list.append(file)
 
740
        
 
741
        if sort_type == 'filename':
 
742
            file_list.sort(key=lambda x: x.filename)
 
743
        elif sort_type == 'size':
 
744
            file_list.sort(key=lambda x: x.size)
 
745
        elif sort_type == 'date':
 
746
            file_list.sort(key=lambda x: x.change.date)
 
747
        
 
748
        parity = 0
 
749
        for file in file_list:
 
750
            file.parity = parity
622
751
            parity ^= 1
623
 
        pass
 
752
 
 
753
        return file_list
 
754
 
 
755
 
 
756
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
624
757
 
625
758
    @with_branch_lock
626
759
    def annotate_file(self, file_id, revid):
630
763
        
631
764
        file_revid = self.get_inventory(revid)[file_id].revision
632
765
        oldvalues = None
633
 
        revision_cache = {}
634
766
        
635
767
        # because we cache revision metadata ourselves, it's actually much
636
768
        # faster to call 'annotate_iter' on the weave directly than it is to
637
769
        # ask bzrlib to annotate for us.
638
770
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
771
        
 
772
        revid_set = set()
 
773
        for line_revid, text in w.annotate_iter(file_revid):
 
774
            revid_set.add(line_revid)
 
775
            if self._BADCHARS_RE.match(text):
 
776
                # bail out; this isn't displayable text
 
777
                yield util.Container(parity=0, lineno=1, status='same',
 
778
                                     text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
 
779
                                     change=util.Container())
 
780
                return
 
781
        change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
 
782
        
639
783
        last_line_revid = None
640
784
        for line_revid, text in w.annotate_iter(file_revid):
641
785
            if line_revid == last_line_revid:
645
789
                status = 'changed'
646
790
                parity ^= 1
647
791
                last_line_revid = line_revid
648
 
                change = revision_cache.get(line_revid, None)
649
 
                if change is None:
650
 
                    change = self.get_change(line_revid)
651
 
                    revision_cache[line_revid] = change
 
792
                change = change_cache[line_revid]
652
793
                trunc_revno = change.revno
653
794
                if len(trunc_revno) > 10:
654
795
                    trunc_revno = trunc_revno[:9] + '...'
655
796
                
656
797
            yield util.Container(parity=parity, lineno=lineno, status=status,
657
 
                                 trunc_revno=trunc_revno, change=change, text=util.html_clean(text))
 
798
                                 change=change, text=util.html_clean(text))
658
799
            lineno += 1
659
800
        
660
 
        log.debug('annotate: %r secs' % (time.time() - z,))
 
801
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
802
 
 
803
    @with_branch_lock
 
804
    @with_bzrlib_read_lock
 
805
    def get_bundle(self, revid):
 
806
        parents = self._revision_graph[revid]
 
807
        if len(parents) > 0:
 
808
            parent_revid = parents[0]
 
809
        else:
 
810
            parent_revid = None
 
811
        s = StringIO()
 
812
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, parent_revid, s)
 
813
        return s.getvalue()
 
814