~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Martin Albisetti
  • Date: 2008-06-19 16:35:15 UTC
  • mto: (157.1.3 loggerhead)
  • mto: This revision was merged to the branch mainline in revision 187.
  • Revision ID: argentina@gmail.com-20080619163515-afe879s0tqwrcrgd
 * Add new images and CSS for diff/revision view
 * Tweak global css

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd.
2
 
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
 
1
#
3
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
4
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
5
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
33
32
import re
34
33
import textwrap
35
34
import threading
36
 
import tarfile
37
 
 
38
 
from bzrlib import tag
 
35
import time
 
36
from StringIO import StringIO
 
37
 
 
38
from loggerhead import util
 
39
from loggerhead.util import decorator
 
40
 
 
41
import bzrlib
39
42
import bzrlib.branch
40
 
import bzrlib.delta
 
43
import bzrlib.bundle.serializer
 
44
import bzrlib.diff
41
45
import bzrlib.errors
42
 
import bzrlib.foreign
 
46
import bzrlib.progress
43
47
import bzrlib.revision
44
 
 
45
 
from loggerhead import search
46
 
from loggerhead import util
47
 
from loggerhead.wholehistory import compute_whole_history_data
 
48
import bzrlib.tsort
 
49
import bzrlib.ui
 
50
 
 
51
 
 
52
with_branch_lock = util.with_lock('_lock', 'branch')
 
53
 
 
54
 
 
55
@decorator
 
56
def with_bzrlib_read_lock(unbound):
 
57
    def bzrlib_read_locked(self, *args, **kw):
 
58
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
 
59
        self._branch.repository.lock_read()
 
60
        try:
 
61
            return unbound(self, *args, **kw)
 
62
        finally:
 
63
            self._branch.repository.unlock()
 
64
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
 
65
    return bzrlib_read_locked
 
66
 
 
67
 
 
68
# bzrlib's UIFactory is not thread-safe
 
69
uihack = threading.local()
 
70
 
 
71
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
 
72
    def nested_progress_bar(self):
 
73
        if getattr(uihack, '_progress_bar_stack', None) is None:
 
74
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
 
75
        return uihack._progress_bar_stack.get_nested()
 
76
 
 
77
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
 
78
 
 
79
 
 
80
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
 
81
    while len(delete_list) < len(insert_list):
 
82
        delete_list.append((None, '', 'context'))
 
83
    while len(insert_list) < len(delete_list):
 
84
        insert_list.append((None, '', 'context'))
 
85
    while len(delete_list) > 0:
 
86
        d = delete_list.pop(0)
 
87
        i = insert_list.pop(0)
 
88
        line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
 
89
                                        old_line=d[1], new_line=i[1],
 
90
                                        old_type=d[2], new_type=i[2]))
 
91
 
 
92
 
 
93
def _make_side_by_side(chunk_list):
 
94
    """
 
95
    turn a normal unified-style diff (post-processed by parse_delta) into a
 
96
    side-by-side diff structure.  the new structure is::
 
97
 
 
98
        chunks: list(
 
99
            diff: list(
 
100
                old_lineno: int,
 
101
                new_lineno: int,
 
102
                old_line: str,
 
103
                new_line: str,
 
104
                type: str('context' or 'changed'),
 
105
            )
 
106
        )
 
107
    """
 
108
    out_chunk_list = []
 
109
    for chunk in chunk_list:
 
110
        line_list = []
 
111
        delete_list, insert_list = [], []
 
112
        for line in chunk.diff:
 
113
            if line.type == 'context':
 
114
                if len(delete_list) or len(insert_list):
 
115
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
116
                    delete_list, insert_list = [], []
 
117
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
 
118
                                                old_line=line.line, new_line=line.line,
 
119
                                                old_type=line.type, new_type=line.type))
 
120
            elif line.type == 'delete':
 
121
                delete_list.append((line.old_lineno, line.line, line.type))
 
122
            elif line.type == 'insert':
 
123
                insert_list.append((line.new_lineno, line.line, line.type))
 
124
        if len(delete_list) or len(insert_list):
 
125
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
 
126
        out_chunk_list.append(util.Container(diff=line_list))
 
127
    return out_chunk_list
48
128
 
49
129
 
50
130
def is_branch(folder):
62
142
    module (Robey, the original author of this code, apparently favored this
63
143
    style of message).
64
144
    """
65
 
    message = message.lstrip().splitlines()
 
145
    message = message.splitlines()
66
146
 
67
147
    if len(message) == 1:
68
148
        message = textwrap.wrap(message[0])
89
169
    return path
90
170
 
91
171
 
 
172
 
 
173
# from bzrlib
92
174
class _RevListToTimestamps(object):
93
175
    """This takes a list of revisions, and allows you to bisect by date"""
94
176
 
100
182
 
101
183
    def __getitem__(self, index):
102
184
        """Get the date of the index'd item"""
103
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
104
 
                   self.revid_list[index]).timestamp)
 
185
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
105
186
 
106
187
    def __len__(self):
107
188
        return len(self.revid_list)
108
189
 
109
 
class FileChangeReporter(object):
110
 
 
111
 
    def __init__(self, old_inv, new_inv):
112
 
        self.added = []
113
 
        self.modified = []
114
 
        self.renamed = []
115
 
        self.removed = []
116
 
        self.text_changes = []
117
 
        self.old_inv = old_inv
118
 
        self.new_inv = new_inv
119
 
 
120
 
    def revid(self, inv, file_id):
121
 
        try:
122
 
            return inv[file_id].revision
123
 
        except bzrlib.errors.NoSuchId:
124
 
            return 'null:'
125
 
 
126
 
    def report(self, file_id, paths, versioned, renamed, modified,
127
 
               exe_change, kind):
128
 
        if modified not in ('unchanged', 'kind changed'):
129
 
            if versioned == 'removed':
130
 
                filename = rich_filename(paths[0], kind[0])
131
 
            else:
132
 
                filename = rich_filename(paths[1], kind[1])
133
 
            self.text_changes.append(util.Container(
134
 
                filename=filename, file_id=file_id,
135
 
                old_revision=self.revid(self.old_inv, file_id),
136
 
                new_revision=self.revid(self.new_inv, file_id)))
137
 
        if versioned == 'added':
138
 
            self.added.append(util.Container(
139
 
                filename=rich_filename(paths[1], kind),
140
 
                file_id=file_id, kind=kind[1]))
141
 
        elif versioned == 'removed':
142
 
            self.removed.append(util.Container(
143
 
                filename=rich_filename(paths[0], kind),
144
 
                file_id=file_id, kind=kind[0]))
145
 
        elif renamed:
146
 
            self.renamed.append(util.Container(
147
 
                old_filename=rich_filename(paths[0], kind[0]),
148
 
                new_filename=rich_filename(paths[1], kind[1]),
149
 
                file_id=file_id,
150
 
                text_modified=modified == 'modified'))
151
 
        else:
152
 
            self.modified.append(util.Container(
153
 
                filename=rich_filename(paths[1], kind),
154
 
                file_id=file_id))
155
 
 
156
 
# The lru_cache is not thread-safe, so we need a lock around it for
157
 
# all threads.
158
 
rev_info_memory_cache_lock = threading.RLock()
159
 
 
160
 
class RevInfoMemoryCache(object):
161
 
    """A store that validates values against the revids they were stored with.
162
 
 
163
 
    We use a unique key for each branch.
164
 
 
165
 
    The reason for not just using the revid as the key is so that when a new
166
 
    value is provided for a branch, we replace the old value used for the
167
 
    branch.
168
 
 
169
 
    There is another implementation of the same interface in
170
 
    loggerhead.changecache.RevInfoDiskCache.
171
 
    """
172
 
 
173
 
    def __init__(self, cache):
174
 
        self._cache = cache
175
 
 
176
 
    def get(self, key, revid):
177
 
        """Return the data associated with `key`, subject to a revid check.
178
 
 
179
 
        If a value was stored under `key`, with the same revid, return it.
180
 
        Otherwise return None.
181
 
        """
182
 
        rev_info_memory_cache_lock.acquire()
183
 
        try:
184
 
            cached = self._cache.get(key)
185
 
        finally:
186
 
            rev_info_memory_cache_lock.release()
187
 
        if cached is None:
188
 
            return None
189
 
        stored_revid, data = cached
190
 
        if revid == stored_revid:
191
 
            return data
192
 
        else:
193
 
            return None
194
 
 
195
 
    def set(self, key, revid, data):
196
 
        """Store `data` under `key`, to be checked against `revid` on get().
197
 
        """
198
 
        rev_info_memory_cache_lock.acquire()
199
 
        try:
200
 
            self._cache[key] = (revid, data)
201
 
        finally:
202
 
            rev_info_memory_cache_lock.release()
203
 
 
204
 
# Used to store locks that prevent multiple threads from building a 
205
 
# revision graph for the same branch at the same time, because that can
206
 
# cause severe performance issues that are so bad that the system seems
207
 
# to hang.
208
 
revision_graph_locks = {}
209
 
revision_graph_check_lock = threading.Lock()
210
 
 
211
 
class History(object):
212
 
    """Decorate a branch to provide information for rendering.
213
 
 
214
 
    History objects are expected to be short lived -- when serving a request
215
 
    for a particular branch, open it, read-lock it, wrap a History object
216
 
    around it, serve the request, throw the History object away, unlock the
217
 
    branch and throw it away.
218
 
 
219
 
    :ivar _rev_info: A list of information about revisions.  This is by far
220
 
        the most cryptic data structure in loggerhead.  At the top level, it
221
 
        is a list of 3-tuples [(merge-info, where-merged, parents)].
222
 
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
223
 
        like a merged sorted list, but the revno is stringified.
224
 
        `where-merged` is a tuple of revisions that have this revision as a
225
 
        non-lefthand parent.  Finally, `parents` is just the usual list of
226
 
        parents of this revision.
227
 
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
228
 
        the information about it in _rev_info.
229
 
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
230
 
        ids.
231
 
    """
232
 
 
233
 
    def _load_whole_history_data(self, caches, cache_key):
234
 
        """Set the attributes relating to the whole history of the branch.
235
 
 
236
 
        :param caches: a list of caches with interfaces like
237
 
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
238
 
        :param cache_key: the key to use with the caches.
239
 
        """
240
 
        self._rev_indices = None
241
 
        self._rev_info = None
242
 
 
243
 
        missed_caches = []
244
 
        def update_missed_caches():
245
 
            for cache in missed_caches:
246
 
                cache.set(cache_key, self.last_revid, self._rev_info)
247
 
 
248
 
        # Theoretically, it's possible for two threads to race in creating
249
 
        # the Lock() object for their branch, so we put a lock around
250
 
        # creating the per-branch Lock().
251
 
        revision_graph_check_lock.acquire()
252
 
        try:
253
 
            if cache_key not in revision_graph_locks:
254
 
                revision_graph_locks[cache_key] = threading.Lock()
255
 
        finally:
256
 
            revision_graph_check_lock.release()
257
 
 
258
 
        revision_graph_locks[cache_key].acquire()
259
 
        try:
260
 
            for cache in caches:
261
 
                data = cache.get(cache_key, self.last_revid)
262
 
                if data is not None:
263
 
                    self._rev_info = data
264
 
                    update_missed_caches()
265
 
                    break
266
 
                else:
267
 
                    missed_caches.append(cache)
268
 
            else:
269
 
                whole_history_data = compute_whole_history_data(self._branch)
270
 
                self._rev_info, self._rev_indices = whole_history_data
271
 
                update_missed_caches()
272
 
        finally:
273
 
            revision_graph_locks[cache_key].release()
274
 
 
275
 
        if self._rev_indices is not None:
276
 
            self._revno_revid = {}
277
 
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
278
 
                self._revno_revid[revno_str] = revid
279
 
        else:
280
 
            self._revno_revid = {}
281
 
            self._rev_indices = {}
282
 
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
283
 
                self._rev_indices[revid] = seq
284
 
                self._revno_revid[revno_str] = revid
285
 
 
286
 
    def __init__(self, branch, whole_history_data_cache,
287
 
                 revinfo_disk_cache=None, cache_key=None):
288
 
        assert branch.is_locked(), (
289
 
            "Can only construct a History object with a read-locked branch.")
 
190
 
 
191
class History (object):
 
192
 
 
193
    def __init__(self):
 
194
        self._file_change_cache = None
 
195
        self._lock = threading.RLock()
 
196
 
 
197
    @classmethod
 
198
    def from_branch(cls, branch, name=None):
 
199
        z = time.time()
 
200
        self = cls()
290
201
        self._branch = branch
291
 
        self._branch_tags = None
292
 
        self._inventory_cache = {}
293
 
        self._branch_nick = self._branch.get_config().get_nickname()
294
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
295
 
 
296
 
        self.last_revid = branch.last_revision()
297
 
 
298
 
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
299
 
        if revinfo_disk_cache:
300
 
            caches.append(revinfo_disk_cache)
301
 
        self._load_whole_history_data(caches, cache_key)
 
202
        self._last_revid = self._branch.last_revision()
 
203
 
 
204
        if name is None:
 
205
            name = self._branch.nick
 
206
        self._name = name
 
207
        self.log = logging.getLogger('loggerhead.%s' % (name,))
 
208
 
 
209
        graph = branch.repository.get_graph()
 
210
        parent_map = dict(((key, value) for key, value in
 
211
             graph.iter_ancestry([self._last_revid]) if value is not None))
 
212
 
 
213
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
 
214
        self._full_history = []
 
215
        self._revision_info = {}
 
216
        self._revno_revid = {}
 
217
        if bzrlib.revision.is_null(self._last_revid):
 
218
            self._merge_sort = []
 
219
        else:
 
220
            self._merge_sort = bzrlib.tsort.merge_sort(
 
221
                self._revision_graph, self._last_revid, generate_revno=True)
 
222
 
 
223
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
 
224
            self._full_history.append(revid)
 
225
            revno_str = '.'.join(str(n) for n in revno)
 
226
            self._revno_revid[revno_str] = revid
 
227
            self._revision_info[revid] = (
 
228
                seq, revid, merge_depth, revno_str, end_of_merge)
 
229
 
 
230
        # cache merge info
 
231
        self._where_merged = {}
 
232
 
 
233
        for revid in self._revision_graph.keys():
 
234
            if self._revision_info[revid][2] == 0:
 
235
                continue
 
236
            for parent in self._revision_graph[revid]:
 
237
                self._where_merged.setdefault(parent, set()).add(revid)
 
238
 
 
239
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
 
240
        return self
 
241
 
 
242
    @staticmethod
 
243
    def _strip_NULL_ghosts(revision_graph):
 
244
        """
 
245
        Copied over from bzrlib meant as a temporary workaround deprecated 
 
246
        methods.
 
247
        """
 
248
 
 
249
        # Filter ghosts, and null:
 
250
        if bzrlib.revision.NULL_REVISION in revision_graph:
 
251
            del revision_graph[bzrlib.revision.NULL_REVISION]
 
252
        for key, parents in revision_graph.items():
 
253
            revision_graph[key] = tuple(parent for parent in parents if parent
 
254
                in revision_graph)
 
255
        return revision_graph
 
256
 
 
257
    @classmethod
 
258
    def from_folder(cls, path, name=None):
 
259
        b = bzrlib.branch.Branch.open(path)
 
260
        b.lock_read()
 
261
        try:
 
262
            return cls.from_branch(b, name)
 
263
        finally:
 
264
            b.unlock()
 
265
 
 
266
    @with_branch_lock
 
267
    def out_of_date(self):
 
268
        # the branch may have been upgraded on disk, in which case we're stale.
 
269
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
 
270
        if self._branch.__class__ is not \
 
271
               newly_opened.__class__:
 
272
            return True
 
273
        if self._branch.repository.__class__ is not \
 
274
               newly_opened.repository.__class__:
 
275
            return True
 
276
        return self._branch.last_revision() != self._last_revid
 
277
 
 
278
    def use_file_cache(self, cache):
 
279
        self._file_change_cache = cache
302
280
 
303
281
    @property
304
282
    def has_revisions(self):
305
283
        return not bzrlib.revision.is_null(self.last_revid)
306
284
 
 
285
    last_revid = property(lambda self: self._last_revid, None, None)
 
286
 
 
287
    @with_branch_lock
307
288
    def get_config(self):
308
289
        return self._branch.get_config()
309
290
 
 
291
 
310
292
    def get_revno(self, revid):
311
 
        if revid not in self._rev_indices:
 
293
        if revid not in self._revision_info:
312
294
            # ghost parent?
313
295
            return 'unknown'
314
 
        seq = self._rev_indices[revid]
315
 
        revno = self._rev_info[seq][0][3]
316
 
        return revno
 
296
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
297
        return revno_str
 
298
 
 
299
    def get_revision_history(self):
 
300
        return self._full_history
317
301
 
318
302
    def get_revids_from(self, revid_list, start_revid):
319
303
        """
321
305
        revid in revid_list.
322
306
        """
323
307
        if revid_list is None:
324
 
            # Just yield the mainline, starting at start_revid
325
 
            revid = start_revid
326
 
            is_null = bzrlib.revision.is_null
327
 
            while not is_null(revid):
328
 
                yield revid
329
 
                parents = self._rev_info[self._rev_indices[revid]][2]
330
 
                if not parents:
331
 
                    return
332
 
                revid = parents[0]
333
 
            return
 
308
            revid_list = self._full_history
334
309
        revid_set = set(revid_list)
335
310
        revid = start_revid
336
 
 
337
311
        def introduced_revisions(revid):
338
312
            r = set([revid])
339
 
            seq = self._rev_indices[revid]
340
 
            md = self._rev_info[seq][0][2]
 
313
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
341
314
            i = seq + 1
342
 
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
343
 
                r.add(self._rev_info[i][0][1])
 
315
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
 
316
                r.add(self._merge_sort[i][1])
344
317
                i += 1
345
318
            return r
346
 
        while revid_set:
 
319
        while 1:
347
320
            if bzrlib.revision.is_null(revid):
348
321
                return
349
 
            rev_introduced = introduced_revisions(revid)
350
 
            matching = rev_introduced.intersection(revid_set)
351
 
            if matching:
352
 
                # We don't need to look for these anymore.
353
 
                revid_set.difference_update(matching)
 
322
            if introduced_revisions(revid) & revid_set:
354
323
                yield revid
355
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
324
            parents = self._revision_graph[revid]
356
325
            if len(parents) == 0:
357
326
                return
358
327
            revid = parents[0]
359
328
 
 
329
    @with_branch_lock
360
330
    def get_short_revision_history_by_fileid(self, file_id):
 
331
        # wow.  is this really the only way we can get this list?  by
 
332
        # man-handling the weave store directly? :-0
361
333
        # FIXME: would be awesome if we could get, for a folder, the list of
362
 
        # revisions where items within that folder changed.i
363
 
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
364
 
        get_parent_map = self._branch.repository.texts.get_parent_map
365
 
        # We chunk the requests as this works better with GraphIndex.
366
 
        # See _filter_revisions_touching_file_id in bzrlib/log.py
367
 
        # for more information.
368
 
        revids = []
369
 
        chunk_size = 1000
370
 
        for start in xrange(0, len(possible_keys), chunk_size):
371
 
            next_keys = possible_keys[start:start + chunk_size]
372
 
            revids += [k[1] for k in get_parent_map(next_keys)]
373
 
        del possible_keys, next_keys
 
334
        # revisions where items within that folder changed.
 
335
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
336
        w_revids = w.versions()
 
337
        revids = [r for r in self._full_history if r in w_revids]
374
338
        return revids
375
339
 
 
340
    @with_branch_lock
376
341
    def get_revision_history_since(self, revid_list, date):
377
342
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
378
343
        # so start at midnight on 02-sep.
379
344
        date = date + datetime.timedelta(days=1)
380
 
        # our revid list is sorted in REVERSE date order,
381
 
        # so go thru some hoops here...
 
345
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
382
346
        revid_list.reverse()
383
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
384
 
                                                   self._branch.repository),
385
 
                              date)
 
347
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
386
348
        if index == 0:
387
349
            return []
388
350
        revid_list.reverse()
389
351
        index = -index
390
352
        return revid_list[index:]
391
353
 
 
354
    @with_branch_lock
392
355
    def get_search_revid_list(self, query, revid_list):
393
356
        """
394
357
        given a "quick-search" query, try a few obvious possible meanings:
395
358
 
396
359
            - revision id or # ("128.1.3")
397
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
398
 
iso style "yyyy-mm-dd")
 
360
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
399
361
            - comment text as a fallback
400
362
 
401
363
        and return a revid list that matches.
404
366
        # all the relevant changes (time-consuming) only to return a list of
405
367
        # revids which will be used to fetch a set of changes again.
406
368
 
407
 
        # if they entered a revid, just jump straight there;
408
 
        # ignore the passed-in revid_list
 
369
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
409
370
        revid = self.fix_revid(query)
410
371
        if revid is not None:
411
372
            if isinstance(revid, unicode):
412
373
                revid = revid.encode('utf-8')
413
 
            changes = self.get_changes([revid])
 
374
            changes = self.get_changes([ revid ])
414
375
            if (changes is not None) and (len(changes) > 0):
415
 
                return [revid]
 
376
                return [ revid ]
416
377
 
417
378
        date = None
418
379
        m = self.us_date_re.match(query)
419
380
        if m is not None:
420
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
421
 
                                     int(m.group(1)),
422
 
                                     int(m.group(2)))
 
381
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
423
382
        else:
424
383
            m = self.earth_date_re.match(query)
425
384
            if m is not None:
426
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
427
 
                                         int(m.group(2)),
428
 
                                         int(m.group(1)))
 
385
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
429
386
            else:
430
387
                m = self.iso_date_re.match(query)
431
388
                if m is not None:
432
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
433
 
                                             int(m.group(2)),
434
 
                                             int(m.group(3)))
 
389
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
435
390
        if date is not None:
436
391
            if revid_list is None:
437
 
                # if no limit to the query was given,
438
 
                # search only the direct-parent path.
439
 
                revid_list = list(self.get_revids_from(None, self.last_revid))
 
392
                # if no limit to the query was given, search only the direct-parent path.
 
393
                revid_list = list(self.get_revids_from(None, self._last_revid))
440
394
            return self.get_revision_history_since(revid_list, date)
441
395
 
442
396
    revno_re = re.compile(r'^[\d\.]+$')
452
406
        if revid is None:
453
407
            return revid
454
408
        if revid == 'head:':
455
 
            return self.last_revid
456
 
        try:
457
 
            if self.revno_re.match(revid):
458
 
                revid = self._revno_revid[revid]
459
 
        except KeyError:
460
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
409
            return self._last_revid
 
410
        if self.revno_re.match(revid):
 
411
            revid = self._revno_revid[revid]
461
412
        return revid
462
413
 
 
414
    @with_branch_lock
463
415
    def get_file_view(self, revid, file_id):
464
416
        """
465
417
        Given a revid and optional path, return a (revlist, revid) for
469
421
        If file_id is None, the entire revision history is the list scope.
470
422
        """
471
423
        if revid is None:
472
 
            revid = self.last_revid
 
424
            revid = self._last_revid
473
425
        if file_id is not None:
474
 
            revlist = list(
475
 
                self.get_short_revision_history_by_fileid(file_id))
476
 
            revlist = self.get_revids_from(revlist, revid)
 
426
            # since revid is 'start_revid', possibly should start the path
 
427
            # tracing from revid... FIXME
 
428
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
 
429
            revlist = list(self.get_revids_from(revlist, revid))
477
430
        else:
478
 
            revlist = self.get_revids_from(None, revid)
 
431
            revlist = list(self.get_revids_from(None, revid))
479
432
        return revlist
480
433
 
481
 
    @staticmethod
482
 
    def _iterate_sufficiently(iterable, stop_at, extra_rev_count):
483
 
        """Return a list of iterable.
484
 
 
485
 
        If extra_rev_count is None, fully consume iterable.
486
 
        Otherwise, stop at 'stop_at' + extra_rev_count.
487
 
 
488
 
        Example:
489
 
          iterate until you find stop_at, then iterate 10 more times.
490
 
        """
491
 
        if extra_rev_count is None:
492
 
            return list(iterable)
493
 
        result = []
494
 
        found = False
495
 
        for n in iterable:
496
 
            result.append(n)
497
 
            if n == stop_at:
498
 
                found = True
499
 
                break
500
 
        if found:
501
 
            for count, n in enumerate(iterable):
502
 
                if count >= extra_rev_count:
503
 
                    break
504
 
                result.append(n)
505
 
        return result
506
 
 
507
 
    def get_view(self, revid, start_revid, file_id, query=None,
508
 
                 extra_rev_count=None):
 
434
    @with_branch_lock
 
435
    def get_view(self, revid, start_revid, file_id, query=None):
509
436
        """
510
437
        use the URL parameters (revid, start_revid, file_id, and query) to
511
438
        determine the revision list we're viewing (start_revid, file_id, query)
516
443
              file.
517
444
            - if a start_revid is given, we're viewing the branch from a
518
445
              specific revision up the tree.
519
 
            - if extra_rev_count is given, find the view from start_revid =>
520
 
              revid, and continue an additional 'extra_rev_count'. If not
521
 
              given, then revid_list will contain the full history of
522
 
              start_revid
523
446
 
524
447
        these may be combined to view revisions for a specific file, from
525
448
        a specific revision, with a specific search query.
534
457
        contain vital context for future url navigation.
535
458
        """
536
459
        if start_revid is None:
537
 
            start_revid = self.last_revid
 
460
            start_revid = self._last_revid
538
461
 
539
462
        if query is None:
540
463
            revid_list = self.get_file_view(start_revid, file_id)
541
 
            revid_list = self._iterate_sufficiently(revid_list, revid,
542
 
                                                    extra_rev_count)
543
464
            if revid is None:
544
465
                revid = start_revid
545
466
            if revid not in revid_list:
546
467
                # if the given revid is not in the revlist, use a revlist that
547
468
                # starts at the given revid.
548
469
                revid_list = self.get_file_view(revid, file_id)
549
 
                revid_list = self._iterate_sufficiently(revid_list, revid,
550
 
                                                        extra_rev_count)
551
470
                start_revid = revid
552
471
            return revid, start_revid, revid_list
553
472
 
556
475
            revid_list = self.get_file_view(start_revid, file_id)
557
476
        else:
558
477
            revid_list = None
559
 
        revid_list = search.search_revisions(self._branch, query)
 
478
 
 
479
        revid_list = self.get_search_revid_list(query, revid_list)
560
480
        if revid_list and len(revid_list) > 0:
561
481
            if revid not in revid_list:
562
482
                revid = revid_list[0]
563
483
            return revid, start_revid, revid_list
564
484
        else:
565
 
            # XXX: This should return a message saying that the search could
566
 
            # not be completed due to either missing the plugin or missing a
567
 
            # search index.
568
485
            return None, None, []
569
486
 
 
487
    @with_branch_lock
570
488
    def get_inventory(self, revid):
571
 
        if revid not in self._inventory_cache:
572
 
            self._inventory_cache[revid] = (
573
 
                self._branch.repository.get_inventory(revid))
574
 
        return self._inventory_cache[revid]
 
489
        return self._branch.repository.get_revision_inventory(revid)
575
490
 
 
491
    @with_branch_lock
576
492
    def get_path(self, revid, file_id):
577
493
        if (file_id is None) or (file_id == ''):
578
494
            return ''
579
 
        path = self.get_inventory(revid).id2path(file_id)
 
495
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
580
496
        if (len(path) > 0) and not path.startswith('/'):
581
497
            path = '/' + path
582
498
        return path
583
499
 
 
500
    @with_branch_lock
584
501
    def get_file_id(self, revid, path):
585
502
        if (len(path) > 0) and not path.startswith('/'):
586
503
            path = '/' + path
587
 
        return self.get_inventory(revid).path2id(path)
 
504
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
588
505
 
589
506
    def get_merge_point_list(self, revid):
590
507
        """
595
512
 
596
513
        merge_point = []
597
514
        while True:
598
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
515
            children = self._where_merged.get(revid, [])
599
516
            nexts = []
600
517
            for child in children:
601
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
518
                child_parents = self._revision_graph[child]
602
519
                if child_parents[0] == revid:
603
520
                    nexts.append(child)
604
521
                else:
624
541
            revnol = revno.split(".")
625
542
            revnos = ".".join(revnol[:-2])
626
543
            revnolast = int(revnol[-1])
627
 
            if revnos in d:
 
544
            if d.has_key(revnos):
628
545
                m = d[revnos][0]
629
546
                if revnolast < m:
630
 
                    d[revnos] = (revnolast, revid)
 
547
                    d[revnos] = ( revnolast, revid )
631
548
            else:
632
 
                d[revnos] = (revnolast, revid)
633
 
 
634
 
        return [revid for (_, revid) in d.itervalues()]
635
 
 
636
 
    def add_branch_nicks(self, change):
 
549
                d[revnos] = ( revnolast, revid )
 
550
 
 
551
        return [ d[revnos][1] for revnos in d.keys() ]
 
552
 
 
553
    def get_branch_nicks(self, changes):
637
554
        """
638
 
        given a 'change', fill in the branch nicks on all parents and merge
639
 
        points.
 
555
        given a list of changes from L{get_changes}, fill in the branch nicks
 
556
        on all parents and merge points.
640
557
        """
641
558
        fetch_set = set()
642
 
        for p in change.parents:
643
 
            fetch_set.add(p.revid)
644
 
        for p in change.merge_points:
645
 
            fetch_set.add(p.revid)
 
559
        for change in changes:
 
560
            for p in change.parents:
 
561
                fetch_set.add(p.revid)
 
562
            for p in change.merge_points:
 
563
                fetch_set.add(p.revid)
646
564
        p_changes = self.get_changes(list(fetch_set))
647
565
        p_change_dict = dict([(c.revid, c) for c in p_changes])
648
 
        for p in change.parents:
649
 
            if p.revid in p_change_dict:
650
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
651
 
            else:
652
 
                p.branch_nick = '(missing)'
653
 
        for p in change.merge_points:
654
 
            if p.revid in p_change_dict:
655
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
656
 
            else:
657
 
                p.branch_nick = '(missing)'
 
566
        for change in changes:
 
567
            # arch-converted branches may not have merged branch info :(
 
568
            for p in change.parents:
 
569
                if p.revid in p_change_dict:
 
570
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
571
                else:
 
572
                    p.branch_nick = '(missing)'
 
573
            for p in change.merge_points:
 
574
                if p.revid in p_change_dict:
 
575
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
576
                else:
 
577
                    p.branch_nick = '(missing)'
658
578
 
 
579
    @with_branch_lock
659
580
    def get_changes(self, revid_list):
660
581
        """Return a list of changes objects for the given revids.
661
582
 
668
589
        # some data needs to be recalculated each time, because it may
669
590
        # change as new revisions are added.
670
591
        for change in changes:
671
 
            merge_revids = self.simplify_merge_point_list(
672
 
                               self.get_merge_point_list(change.revid))
673
 
            change.merge_points = [
674
 
                util.Container(revid=r,
675
 
                revno=self.get_revno(r)) for r in merge_revids]
 
592
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
 
593
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
676
594
            if len(change.parents) > 0:
677
 
                change.parents = [util.Container(revid=r,
 
595
                change.parents = [util.Container(revid=r, 
678
596
                    revno=self.get_revno(r)) for r in change.parents]
679
597
            change.revno = self.get_revno(change.revid)
680
598
 
685
603
 
686
604
        return changes
687
605
 
 
606
    @with_branch_lock
 
607
    @with_bzrlib_read_lock
688
608
    def get_changes_uncached(self, revid_list):
689
609
        # FIXME: deprecated method in getting a null revision
690
610
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
691
611
                            revid_list)
692
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
693
 
                         revid_list)
 
612
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
694
613
        # We need to return the answer in the same order as the input,
695
614
        # less any ghosts.
696
615
        present_revids = [revid for revid in revid_list
699
618
 
700
619
        return [self._change_from_revision(rev) for rev in rev_list]
701
620
 
 
621
    def _get_deltas_for_revisions_with_trees(self, revisions):
 
622
        """Produce a list of revision deltas.
 
623
 
 
624
        Note that the input is a sequence of REVISIONS, not revision_ids.
 
625
        Trees will be held in memory until the generator exits.
 
626
        Each delta is relative to the revision's lefthand predecessor.
 
627
        (This is copied from bzrlib.)
 
628
        """
 
629
        required_trees = set()
 
630
        for revision in revisions:
 
631
            required_trees.add(revision.revid)
 
632
            required_trees.update([p.revid for p in revision.parents[:1]])
 
633
        trees = dict((t.get_revision_id(), t) for
 
634
                     t in self._branch.repository.revision_trees(required_trees))
 
635
        ret = []
 
636
        self._branch.repository.lock_read()
 
637
        try:
 
638
            for revision in revisions:
 
639
                if not revision.parents:
 
640
                    old_tree = self._branch.repository.revision_tree(
 
641
                        bzrlib.revision.NULL_REVISION)
 
642
                else:
 
643
                    old_tree = trees[revision.parents[0].revid]
 
644
                tree = trees[revision.revid]
 
645
                ret.append(tree.changes_from(old_tree))
 
646
            return ret
 
647
        finally:
 
648
            self._branch.repository.unlock()
 
649
 
702
650
    def _change_from_revision(self, revision):
703
651
        """
704
652
        Given a bzrlib Revision, return a processed "change" for use in
705
653
        templates.
706
654
        """
 
655
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
 
656
 
 
657
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
 
658
 
707
659
        message, short_message = clean_message(revision.message)
708
660
 
709
 
        if self._branch_tags is None:
710
 
            self._branch_tags = self._branch.tags.get_reverse_tag_dict()
711
 
 
712
 
        revtags = None
713
 
        if revision.revision_id in self._branch_tags:
714
 
          # tag.sort_* functions expect (tag, data) pairs, so we generate them,
715
 
          # and then strip them
716
 
          tags = [(t, None) for t in self._branch_tags[revision.revision_id]]
717
 
          sort_func = getattr(tag, 'sort_natural', None)
718
 
          if sort_func is None:
719
 
              tags.sort()
720
 
          else:
721
 
              sort_func(self._branch, tags)
722
 
          revtags = u', '.join([t[0] for t in tags])
723
 
 
724
661
        entry = {
725
662
            'revid': revision.revision_id,
726
 
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
727
 
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
728
 
            'committer': revision.committer,
729
 
            'authors': revision.get_apparent_authors(),
 
663
            'date': commit_time,
 
664
            'author': revision.committer,
730
665
            'branch_nick': revision.properties.get('branch-nick', None),
731
666
            'short_comment': short_message,
732
667
            'comment': revision.message,
733
668
            'comment_clean': [util.html_clean(s) for s in message],
734
669
            'parents': revision.parent_ids,
735
 
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
736
 
            'tags': revtags,
737
670
        }
738
 
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
739
 
            foreign_revid, mapping = (
740
 
                revision.foreign_revid, revision.mapping)
741
 
        elif ":" in revision.revision_id:
742
 
            try:
743
 
                foreign_revid, mapping = \
744
 
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
745
 
                        revision.revision_id)
746
 
            except bzrlib.errors.InvalidRevisionId:
747
 
                foreign_revid = None
748
 
                mapping = None
749
 
        else:
750
 
            foreign_revid = None
751
 
        if foreign_revid is not None:
752
 
            entry["foreign_vcs"] = mapping.vcs.abbreviation
753
 
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
754
671
        return util.Container(entry)
755
672
 
756
 
    def get_file_changes(self, entry):
757
 
        if entry.parents:
758
 
            old_revid = entry.parents[0].revid
 
673
    def get_file_changes_uncached(self, entries):
 
674
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
 
675
 
 
676
        return [self.parse_delta(delta) for delta in delta_list]
 
677
 
 
678
    @with_branch_lock
 
679
    def get_file_changes(self, entries):
 
680
        if self._file_change_cache is None:
 
681
            return self.get_file_changes_uncached(entries)
759
682
        else:
760
 
            old_revid = bzrlib.revision.NULL_REVISION
761
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
762
 
 
763
 
    def add_changes(self, entry):
764
 
        changes = self.get_file_changes(entry)
765
 
        entry.changes = changes
766
 
 
 
683
            return self._file_change_cache.get_file_changes(entries)
 
684
 
 
685
    def add_changes(self, entries):
 
686
        changes_list = self.get_file_changes(entries)
 
687
 
 
688
        for entry, changes in zip(entries, changes_list):
 
689
            entry.changes = changes
 
690
 
 
691
    @with_branch_lock
 
692
    def get_change_with_diff(self, revid, compare_revid=None):
 
693
        change = self.get_changes([revid])[0]
 
694
 
 
695
        if compare_revid is None:
 
696
            if change.parents:
 
697
                compare_revid = change.parents[0].revid
 
698
            else:
 
699
                compare_revid = 'null:'
 
700
 
 
701
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
 
702
        rev_tree2 = self._branch.repository.revision_tree(revid)
 
703
        delta = rev_tree2.changes_from(rev_tree1)
 
704
 
 
705
        change.changes = self.parse_delta(delta)
 
706
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
707
 
 
708
        return change
 
709
 
 
710
    @with_branch_lock
767
711
    def get_file(self, file_id, revid):
768
 
        """Returns (path, filename, file contents)"""
 
712
        "returns (path, filename, data)"
769
713
        inv = self.get_inventory(revid)
770
714
        inv_entry = inv[file_id]
771
715
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
774
718
            path = '/' + path
775
719
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
776
720
 
777
 
    def file_changes_for_revision_ids(self, old_revid, new_revid):
 
721
    def _parse_diffs(self, old_tree, new_tree, delta):
 
722
        """
 
723
        Return a list of processed diffs, in the format::
 
724
 
 
725
            list(
 
726
                filename: str,
 
727
                file_id: str,
 
728
                chunks: list(
 
729
                    diff: list(
 
730
                        old_lineno: int,
 
731
                        new_lineno: int,
 
732
                        type: str('context', 'delete', or 'insert'),
 
733
                        line: str,
 
734
                    ),
 
735
                ),
 
736
            )
 
737
        """
 
738
        process = []
 
739
        out = []
 
740
 
 
741
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
742
            if text_modified:
 
743
                process.append((old_path, new_path, fid, kind))
 
744
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
745
            process.append((path, path, fid, kind))
 
746
 
 
747
        for old_path, new_path, fid, kind in process:
 
748
            old_lines = old_tree.get_file_lines(fid)
 
749
            new_lines = new_tree.get_file_lines(fid)
 
750
            buffer = StringIO()
 
751
            if old_lines != new_lines:
 
752
                try:
 
753
                    bzrlib.diff.internal_diff(old_path, old_lines,
 
754
                                              new_path, new_lines, buffer)
 
755
                except bzrlib.errors.BinaryFile:
 
756
                    diff = ''
 
757
                else:
 
758
                    diff = buffer.getvalue()
 
759
            else:
 
760
                diff = ''
 
761
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
 
762
 
 
763
        return out
 
764
 
 
765
    def _process_diff(self, diff):
 
766
        # doesn't really need to be a method; could be static.
 
767
        chunks = []
 
768
        chunk = None
 
769
        for line in diff.splitlines():
 
770
            if len(line) == 0:
 
771
                continue
 
772
            if line.startswith('+++ ') or line.startswith('--- '):
 
773
                continue
 
774
            if line.startswith('@@ '):
 
775
                # new chunk
 
776
                if chunk is not None:
 
777
                    chunks.append(chunk)
 
778
                chunk = util.Container()
 
779
                chunk.diff = []
 
780
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
 
781
                old_lineno = lines[0]
 
782
                new_lineno = lines[1]
 
783
            elif line.startswith(' '):
 
784
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
785
                                                 type='context', line=util.fixed_width(line[1:])))
 
786
                old_lineno += 1
 
787
                new_lineno += 1
 
788
            elif line.startswith('+'):
 
789
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
790
                                                 type='insert', line=util.fixed_width(line[1:])))
 
791
                new_lineno += 1
 
792
            elif line.startswith('-'):
 
793
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
794
                                                 type='delete', line=util.fixed_width(line[1:])))
 
795
                old_lineno += 1
 
796
            else:
 
797
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
798
                                                 type='unknown', line=util.fixed_width(repr(line))))
 
799
        if chunk is not None:
 
800
            chunks.append(chunk)
 
801
        return chunks
 
802
 
 
803
    def parse_delta(self, delta):
778
804
        """
779
805
        Return a nested data structure containing the changes in a delta::
780
806
 
784
810
            modified: list(
785
811
                filename: str,
786
812
                file_id: str,
787
 
            ),
788
 
            text_changes: list((filename, file_id)),
789
 
        """
790
 
        repo = self._branch.repository
791
 
        if (bzrlib.revision.is_null(old_revid) or
792
 
            bzrlib.revision.is_null(new_revid)):
793
 
            old_tree, new_tree = map(
794
 
                repo.revision_tree, [old_revid, new_revid])
795
 
        else:
796
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
797
 
 
798
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
799
 
 
800
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
801
 
 
802
 
        return util.Container(
803
 
            added=sorted(reporter.added, key=lambda x: x.filename),
804
 
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
805
 
            removed=sorted(reporter.removed, key=lambda x: x.filename),
806
 
            modified=sorted(reporter.modified, key=lambda x: x.filename),
807
 
            text_changes=sorted(reporter.text_changes,
808
 
                                key=lambda x: x.filename))
809
 
 
 
813
            )
 
814
        """
 
815
        added = []
 
816
        modified = []
 
817
        renamed = []
 
818
        removed = []
 
819
 
 
820
        for path, fid, kind in delta.added:
 
821
            added.append((rich_filename(path, kind), fid))
 
822
 
 
823
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
824
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
 
825
 
 
826
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
827
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
 
828
            if meta_modified or text_modified:
 
829
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
830
 
 
831
        for path, fid, kind in delta.removed:
 
832
            removed.append((rich_filename(path, kind), fid))
 
833
 
 
834
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
 
835
 
 
836
    @staticmethod
 
837
    def add_side_by_side(changes):
 
838
        # FIXME: this is a rotten API.
 
839
        for change in changes:
 
840
            for m in change.changes.modified:
 
841
                m.sbs_chunks = _make_side_by_side(m.chunks)
 
842
 
 
843
    @with_branch_lock
 
844
    def get_filelist(self, inv, file_id, sort_type=None):
 
845
        """
 
846
        return the list of all files (and their attributes) within a given
 
847
        path subtree.
 
848
        """
 
849
 
 
850
        dir_ie = inv[file_id]
 
851
        path = inv.id2path(file_id)
 
852
        file_list = []
 
853
 
 
854
        revid_set = set()
 
855
 
 
856
        for filename, entry in dir_ie.children.iteritems():
 
857
            revid_set.add(entry.revision)
 
858
 
 
859
        change_dict = {}
 
860
        for change in self.get_changes(list(revid_set)):
 
861
            change_dict[change.revid] = change
 
862
 
 
863
        for filename, entry in dir_ie.children.iteritems():
 
864
            pathname = filename
 
865
            if entry.kind == 'directory':
 
866
                pathname += '/'
 
867
 
 
868
            revid = entry.revision
 
869
 
 
870
            file = util.Container(
 
871
                filename=filename, executable=entry.executable, kind=entry.kind,
 
872
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
 
873
                revid=revid, change=change_dict[revid])
 
874
            file_list.append(file)
 
875
 
 
876
        if sort_type == 'filename' or sort_type is None:
 
877
            file_list.sort(key=lambda x: x.filename)
 
878
        elif sort_type == 'size':
 
879
            file_list.sort(key=lambda x: x.size)
 
880
        elif sort_type == 'date':
 
881
            file_list.sort(key=lambda x: x.change.date)
 
882
 
 
883
        parity = 0
 
884
        for file in file_list:
 
885
            file.parity = parity
 
886
            parity ^= 1
 
887
 
 
888
        return file_list
 
889
 
 
890
 
 
891
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
 
892
 
 
893
    @with_branch_lock
 
894
    def annotate_file(self, file_id, revid):
 
895
        z = time.time()
 
896
        lineno = 1
 
897
        parity = 0
 
898
 
 
899
        file_revid = self.get_inventory(revid)[file_id].revision
 
900
        oldvalues = None
 
901
        tree = self._branch.repository.revision_tree(file_revid)
 
902
        revid_set = set()
 
903
 
 
904
        for line_revid, text in tree.annotate_iter(file_id):
 
905
            revid_set.add(line_revid)
 
906
            if self._BADCHARS_RE.match(text):
 
907
                # bail out; this isn't displayable text
 
908
                yield util.Container(parity=0, lineno=1, status='same',
 
909
                                     text='(This is a binary file.)',
 
910
                                     change=util.Container())
 
911
                return
 
912
        change_cache = dict([(c.revid, c) \
 
913
                for c in self.get_changes(list(revid_set))])
 
914
 
 
915
        last_line_revid = None
 
916
        for line_revid, text in tree.annotate_iter(file_id):
 
917
            if line_revid == last_line_revid:
 
918
                # remember which lines have a new revno and which don't
 
919
                status = 'same'
 
920
            else:
 
921
                status = 'changed'
 
922
                parity ^= 1
 
923
                last_line_revid = line_revid
 
924
                change = change_cache[line_revid]
 
925
                trunc_revno = change.revno
 
926
                if len(trunc_revno) > 10:
 
927
                    trunc_revno = trunc_revno[:9] + '...'
 
928
 
 
929
            yield util.Container(parity=parity, lineno=lineno, status=status,
 
930
                                 change=change, text=util.fixed_width(text))
 
931
            lineno += 1
 
932
 
 
933
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
934
 
 
935
    @with_branch_lock
 
936
    def get_bundle(self, revid, compare_revid=None):
 
937
        if compare_revid is None:
 
938
            parents = self._revision_graph[revid]
 
939
            if len(parents) > 0:
 
940
                compare_revid = parents[0]
 
941
            else:
 
942
                compare_revid = None
 
943
        s = StringIO()
 
944
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
 
945
        return s.getvalue()