~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

final (?) difference

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
35
33
import textwrap
36
34
import threading
37
35
import time
38
 
import urllib
39
36
from StringIO import StringIO
40
37
 
41
 
from loggerhead import search
42
38
from loggerhead import util
43
 
from loggerhead.wholehistory import compute_whole_history_data
 
39
from loggerhead.util import decorator
44
40
 
45
41
import bzrlib
46
42
import bzrlib.branch
47
 
import bzrlib.delta
 
43
import bzrlib.bundle.serializer
48
44
import bzrlib.diff
49
45
import bzrlib.errors
50
46
import bzrlib.progress
51
47
import bzrlib.revision
52
 
import bzrlib.textfile
53
48
import bzrlib.tsort
54
49
import bzrlib.ui
55
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
 
56
68
# bzrlib's UIFactory is not thread-safe
57
69
uihack = threading.local()
58
70
 
59
 
 
60
71
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
61
 
 
62
72
    def nested_progress_bar(self):
63
73
        if getattr(uihack, '_progress_bar_stack', None) is None:
64
 
            pbs = bzrlib.progress.ProgressBarStack(
65
 
                      klass=bzrlib.progress.DummyProgress)
66
 
            uihack._progress_bar_stack = pbs
 
74
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
67
75
        return uihack._progress_bar_stack.get_nested()
68
76
 
69
77
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
70
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
 
128
 
 
129
 
71
130
def is_branch(folder):
72
131
    try:
73
132
        bzrlib.branch.Branch.open(folder)
83
142
    module (Robey, the original author of this code, apparently favored this
84
143
    style of message).
85
144
    """
86
 
    message = message.lstrip().splitlines()
 
145
    message = message.splitlines()
87
146
 
88
147
    if len(message) == 1:
89
148
        message = textwrap.wrap(message[0])
110
169
    return path
111
170
 
112
171
 
 
172
 
113
173
# from bzrlib
114
 
 
115
 
 
116
174
class _RevListToTimestamps(object):
117
175
    """This takes a list of revisions, and allows you to bisect by date"""
118
176
 
124
182
 
125
183
    def __getitem__(self, index):
126
184
        """Get the date of the index'd item"""
127
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
128
 
                   self.revid_list[index]).timestamp)
 
185
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
129
186
 
130
187
    def __len__(self):
131
188
        return len(self.revid_list)
132
189
 
133
 
class FileChangeReporter(object):
134
 
    def __init__(self, old_inv, new_inv):
135
 
        self.added = []
136
 
        self.modified = []
137
 
        self.renamed = []
138
 
        self.removed = []
139
 
        self.text_changes = []
140
 
        self.old_inv = old_inv
141
 
        self.new_inv = new_inv
142
 
 
143
 
    def revid(self, inv, file_id):
144
 
        try:
145
 
            return inv[file_id].revision
146
 
        except bzrlib.errors.NoSuchId:
147
 
            return 'null:'
148
 
 
149
 
    def report(self, file_id, paths, versioned, renamed, modified,
150
 
               exe_change, kind):
151
 
        if modified not in ('unchanged', 'kind changed'):
152
 
            if versioned == 'removed':
153
 
                filename = rich_filename(paths[0], kind[0])
154
 
            else:
155
 
                filename = rich_filename(paths[1], kind[1])
156
 
            self.text_changes.append(util.Container(
157
 
                filename=filename, file_id=file_id,
158
 
                old_revision=self.revid(self.old_inv, file_id),
159
 
                new_revision=self.revid(self.new_inv, file_id)))
160
 
        if versioned == 'added':
161
 
            self.added.append(util.Container(
162
 
                filename=rich_filename(paths[1], kind),
163
 
                file_id=file_id, kind=kind[1]))
164
 
        elif versioned == 'removed':
165
 
            self.removed.append(util.Container(
166
 
                filename=rich_filename(paths[0], kind),
167
 
                file_id=file_id, kind=kind[0]))
168
 
        elif renamed:
169
 
            self.renamed.append(util.Container(
170
 
                old_filename=rich_filename(paths[0], kind[0]),
171
 
                new_filename=rich_filename(paths[1], kind[1]),
172
 
                file_id=file_id,
173
 
                text_modified=modified == 'modified'))
174
 
        else:
175
 
            self.modified.append(util.Container(
176
 
                filename=rich_filename(paths[1], kind),
177
 
                file_id=file_id))
178
 
 
179
190
 
180
191
class History (object):
181
 
    """Decorate a branch to provide information for rendering.
182
 
 
183
 
    History objects are expected to be short lived -- when serving a request
184
 
    for a particular branch, open it, read-lock it, wrap a History object
185
 
    around it, serve the request, throw the History object away, unlock the
186
 
    branch and throw it away.
187
 
 
188
 
    :ivar _file_change_cache: xx
189
 
    """
190
 
 
191
 
    def __init__(self, branch, whole_history_data_cache):
192
 
        assert branch.is_locked(), (
193
 
            "Can only construct a History object with a read-locked branch.")
 
192
 
 
193
    def __init__(self):
 
194
        self._change_cache = None
194
195
        self._file_change_cache = None
 
196
        self._index = None
 
197
        self._lock = threading.RLock()
 
198
 
 
199
    @classmethod
 
200
    def from_branch(cls, branch, name=None):
 
201
        z = time.time()
 
202
        self = cls()
195
203
        self._branch = branch
196
 
        self._inventory_cache = {}
197
 
        self._branch_nick = self._branch.get_config().get_nickname()
198
 
        self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
199
 
 
200
 
        self.last_revid = branch.last_revision()
201
 
 
202
 
        whole_history_data = whole_history_data_cache.get(self.last_revid)
203
 
        if whole_history_data is None:
204
 
            whole_history_data = compute_whole_history_data(branch)
205
 
            whole_history_data_cache[self.last_revid] = whole_history_data
206
 
 
207
 
        (self._revision_graph, self._full_history, self._revision_info,
208
 
         self._revno_revid, self._merge_sort, self._where_merged,
209
 
         ) = whole_history_data
 
204
        self._last_revid = self._branch.last_revision()
 
205
 
 
206
        if name is None:
 
207
            name = self._branch.nick
 
208
        self._name = name
 
209
        self.log = logging.getLogger('loggerhead.%s' % (name,))
 
210
 
 
211
        graph = branch.repository.get_graph()
 
212
        parent_map = dict(((key, value) for key, value in
 
213
             graph.iter_ancestry([self._last_revid]) if value is not None))
 
214
 
 
215
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
 
216
        self._full_history = []
 
217
        self._revision_info = {}
 
218
        self._revno_revid = {}
 
219
        if bzrlib.revision.is_null(self._last_revid):
 
220
            self._merge_sort = []
 
221
        else:
 
222
            self._merge_sort = bzrlib.tsort.merge_sort(
 
223
                self._revision_graph, self._last_revid, generate_revno=True)
 
224
 
 
225
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
 
226
            self._full_history.append(revid)
 
227
            revno_str = '.'.join(str(n) for n in revno)
 
228
            self._revno_revid[revno_str] = revid
 
229
            self._revision_info[revid] = (
 
230
                seq, revid, merge_depth, revno_str, end_of_merge)
 
231
 
 
232
        # cache merge info
 
233
        self._where_merged = {}
 
234
 
 
235
        for revid in self._revision_graph.keys():
 
236
            if self._revision_info[revid][2] == 0:
 
237
                continue
 
238
            for parent in self._revision_graph[revid]:
 
239
                self._where_merged.setdefault(parent, set()).add(revid)
 
240
 
 
241
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
 
242
        return self
 
243
 
 
244
    @staticmethod
 
245
    def _strip_NULL_ghosts(revision_graph):
 
246
        """
 
247
        Copied over from bzrlib meant as a temporary workaround deprecated 
 
248
        methods.
 
249
        """
 
250
 
 
251
        # Filter ghosts, and null:
 
252
        if bzrlib.revision.NULL_REVISION in revision_graph:
 
253
            del revision_graph[bzrlib.revision.NULL_REVISION]
 
254
        for key, parents in revision_graph.items():
 
255
            revision_graph[key] = tuple(parent for parent in parents if parent
 
256
                in revision_graph)
 
257
        return revision_graph
 
258
 
 
259
    @classmethod
 
260
    def from_folder(cls, path, name=None):
 
261
        b = bzrlib.branch.Branch.open(path)
 
262
        b.lock_read()
 
263
        try:
 
264
            return cls.from_branch(b, name)
 
265
        finally:
 
266
            b.unlock()
 
267
 
 
268
    @with_branch_lock
 
269
    def out_of_date(self):
 
270
        # the branch may have been upgraded on disk, in which case we're stale.
 
271
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
 
272
        if self._branch.__class__ is not \
 
273
               newly_opened.__class__:
 
274
            return True
 
275
        if self._branch.repository.__class__ is not \
 
276
               newly_opened.repository.__class__:
 
277
            return True
 
278
        return self._branch.last_revision() != self._last_revid
 
279
 
 
280
    def use_cache(self, cache):
 
281
        self._change_cache = cache
210
282
 
211
283
    def use_file_cache(self, cache):
212
284
        self._file_change_cache = cache
213
285
 
 
286
    def use_search_index(self, index):
 
287
        self._index = index
 
288
 
214
289
    @property
215
290
    def has_revisions(self):
216
291
        return not bzrlib.revision.is_null(self.last_revid)
217
292
 
 
293
    @with_branch_lock
 
294
    def detach(self):
 
295
        # called when a new history object needs to be created, because the
 
296
        # branch history has changed.  we need to immediately close and stop
 
297
        # using our caches, because a new history object will be created to
 
298
        # replace us, using the same cache files.
 
299
        # (may also be called during server shutdown.)
 
300
        if self._change_cache is not None:
 
301
            self._change_cache.close()
 
302
            self._change_cache = None
 
303
        if self._index is not None:
 
304
            self._index.close()
 
305
            self._index = None
 
306
 
 
307
    def flush_cache(self):
 
308
        if self._change_cache is None:
 
309
            return
 
310
        self._change_cache.flush()
 
311
 
 
312
    def check_rebuild(self):
 
313
        if self._change_cache is not None:
 
314
            self._change_cache.check_rebuild()
 
315
        #if self._index is not None:
 
316
        #    self._index.check_rebuild()
 
317
 
 
318
    last_revid = property(lambda self: self._last_revid, None, None)
 
319
 
 
320
    @with_branch_lock
218
321
    def get_config(self):
219
322
        return self._branch.get_config()
220
323
 
 
324
 
221
325
    def get_revno(self, revid):
222
326
        if revid not in self._revision_info:
223
327
            # ghost parent?
224
328
            return 'unknown'
225
 
        (seq, revid, merge_depth,
226
 
         revno_str, end_of_merge) = self._revision_info[revid]
 
329
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
227
330
        return revno_str
228
331
 
 
332
    def get_revision_history(self):
 
333
        return self._full_history
 
334
 
229
335
    def get_revids_from(self, revid_list, start_revid):
230
336
        """
231
337
        Yield the mainline (wrt start_revid) revisions that merged each
235
341
            revid_list = self._full_history
236
342
        revid_set = set(revid_list)
237
343
        revid = start_revid
238
 
 
239
344
        def introduced_revisions(revid):
240
345
            r = set([revid])
241
346
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
254
359
                return
255
360
            revid = parents[0]
256
361
 
 
362
    @with_branch_lock
257
363
    def get_short_revision_history_by_fileid(self, file_id):
 
364
        # wow.  is this really the only way we can get this list?  by
 
365
        # man-handling the weave store directly? :-0
258
366
        # FIXME: would be awesome if we could get, for a folder, the list of
259
 
        # revisions where items within that folder changed.i
260
 
        try:
261
 
            # FIXME: Workaround for bzr versions prior to 1.6b3.
262
 
            # Remove me eventually pretty please  :)
263
 
            w = self._branch.repository.weave_store.get_weave(
264
 
                    file_id, self._branch.repository.get_transaction())
265
 
            w_revids = w.versions()
266
 
            revids = [r for r in self._full_history if r in w_revids]
267
 
        except AttributeError:
268
 
            possible_keys = [(file_id, revid) for revid in self._full_history]
269
 
            get_parent_map = self._branch.repository.texts.get_parent_map
270
 
            # We chunk the requests as this works better with GraphIndex.
271
 
            # See _filter_revisions_touching_file_id in bzrlib/log.py
272
 
            # for more information.
273
 
            revids = []
274
 
            chunk_size = 1000
275
 
            for start in xrange(0, len(possible_keys), chunk_size):
276
 
                next_keys = possible_keys[start:start + chunk_size]
277
 
                revids += [k[1] for k in get_parent_map(next_keys)]
278
 
            del possible_keys, next_keys
 
367
        # revisions where items within that folder changed.
 
368
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
 
369
        w_revids = w.versions()
 
370
        revids = [r for r in self._full_history if r in w_revids]
279
371
        return revids
280
372
 
 
373
    @with_branch_lock
281
374
    def get_revision_history_since(self, revid_list, date):
282
375
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
283
376
        # so start at midnight on 02-sep.
284
377
        date = date + datetime.timedelta(days=1)
285
 
        # our revid list is sorted in REVERSE date order,
286
 
        # so go thru some hoops here...
 
378
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
287
379
        revid_list.reverse()
288
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
289
 
                                                   self._branch.repository),
290
 
                              date)
 
380
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
291
381
        if index == 0:
292
382
            return []
293
383
        revid_list.reverse()
294
384
        index = -index
295
385
        return revid_list[index:]
296
386
 
 
387
    @with_branch_lock
 
388
    def get_revision_history_matching(self, revid_list, text):
 
389
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
390
        z = time.time()
 
391
        # this is going to be painfully slow. :(
 
392
        out = []
 
393
        text = text.lower()
 
394
        for revid in revid_list:
 
395
            change = self.get_changes([ revid ])[0]
 
396
            if text in change.comment.lower():
 
397
                out.append(revid)
 
398
        self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
 
399
        return out
 
400
 
 
401
    def get_revision_history_matching_indexed(self, revid_list, text):
 
402
        self.log.debug('searching %d revisions for %r', len(revid_list), text)
 
403
        z = time.time()
 
404
        if self._index is None:
 
405
            return self.get_revision_history_matching(revid_list, text)
 
406
        out = self._index.find(text, revid_list)
 
407
        self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
 
408
        # put them in some coherent order :)
 
409
        out = [r for r in self._full_history if r in out]
 
410
        return out
 
411
 
 
412
    @with_branch_lock
297
413
    def get_search_revid_list(self, query, revid_list):
298
414
        """
299
415
        given a "quick-search" query, try a few obvious possible meanings:
300
416
 
301
417
            - revision id or # ("128.1.3")
302
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
303
 
iso style "yyyy-mm-dd")
 
418
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
304
419
            - comment text as a fallback
305
420
 
306
421
        and return a revid list that matches.
309
424
        # all the relevant changes (time-consuming) only to return a list of
310
425
        # revids which will be used to fetch a set of changes again.
311
426
 
312
 
        # if they entered a revid, just jump straight there;
313
 
        # ignore the passed-in revid_list
 
427
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
314
428
        revid = self.fix_revid(query)
315
429
        if revid is not None:
316
430
            if isinstance(revid, unicode):
317
431
                revid = revid.encode('utf-8')
318
 
            changes = self.get_changes([revid])
 
432
            changes = self.get_changes([ revid ])
319
433
            if (changes is not None) and (len(changes) > 0):
320
 
                return [revid]
 
434
                return [ revid ]
321
435
 
322
436
        date = None
323
437
        m = self.us_date_re.match(query)
324
438
        if m is not None:
325
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
326
 
                                     int(m.group(1)),
327
 
                                     int(m.group(2)))
 
439
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
328
440
        else:
329
441
            m = self.earth_date_re.match(query)
330
442
            if m is not None:
331
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
332
 
                                         int(m.group(2)),
333
 
                                         int(m.group(1)))
 
443
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
334
444
            else:
335
445
                m = self.iso_date_re.match(query)
336
446
                if m is not None:
337
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
338
 
                                             int(m.group(2)),
339
 
                                             int(m.group(3)))
 
447
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
340
448
        if date is not None:
341
449
            if revid_list is None:
342
 
                # if no limit to the query was given,
343
 
                # search only the direct-parent path.
344
 
                revid_list = list(self.get_revids_from(None, self.last_revid))
 
450
                # if no limit to the query was given, search only the direct-parent path.
 
451
                revid_list = list(self.get_revids_from(None, self._last_revid))
345
452
            return self.get_revision_history_since(revid_list, date)
346
453
 
 
454
        # check comment fields.
 
455
        if revid_list is None:
 
456
            revid_list = self._full_history
 
457
        return self.get_revision_history_matching_indexed(revid_list, query)
 
458
 
347
459
    revno_re = re.compile(r'^[\d\.]+$')
348
460
    # the date regex are without a final '$' so that queries like
349
461
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
357
469
        if revid is None:
358
470
            return revid
359
471
        if revid == 'head:':
360
 
            return self.last_revid
361
 
        try:
362
 
            if self.revno_re.match(revid):
363
 
                revid = self._revno_revid[revid]
364
 
        except KeyError:
365
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
472
            return self._last_revid
 
473
        if self.revno_re.match(revid):
 
474
            revid = self._revno_revid[revid]
366
475
        return revid
367
476
 
 
477
    @with_branch_lock
368
478
    def get_file_view(self, revid, file_id):
369
479
        """
370
480
        Given a revid and optional path, return a (revlist, revid) for
374
484
        If file_id is None, the entire revision history is the list scope.
375
485
        """
376
486
        if revid is None:
377
 
            revid = self.last_revid
 
487
            revid = self._last_revid
378
488
        if file_id is not None:
379
489
            # since revid is 'start_revid', possibly should start the path
380
490
            # tracing from revid... FIXME
384
494
            revlist = list(self.get_revids_from(None, revid))
385
495
        return revlist
386
496
 
 
497
    @with_branch_lock
387
498
    def get_view(self, revid, start_revid, file_id, query=None):
388
499
        """
389
500
        use the URL parameters (revid, start_revid, file_id, and query) to
409
520
        contain vital context for future url navigation.
410
521
        """
411
522
        if start_revid is None:
412
 
            start_revid = self.last_revid
 
523
            start_revid = self._last_revid
413
524
 
414
525
        if query is None:
415
526
            revid_list = self.get_file_view(start_revid, file_id)
427
538
            revid_list = self.get_file_view(start_revid, file_id)
428
539
        else:
429
540
            revid_list = None
430
 
        revid_list = search.search_revisions(self._branch, query)
431
 
        if revid_list and len(revid_list) > 0:
 
541
 
 
542
        revid_list = self.get_search_revid_list(query, revid_list)
 
543
        if len(revid_list) > 0:
432
544
            if revid not in revid_list:
433
545
                revid = revid_list[0]
434
546
            return revid, start_revid, revid_list
435
547
        else:
436
 
            # XXX: This should return a message saying that the search could
437
 
            # not be completed due to either missing the plugin or missing a
438
 
            # search index.
 
548
            # no results
439
549
            return None, None, []
440
550
 
 
551
    @with_branch_lock
441
552
    def get_inventory(self, revid):
442
 
        if revid not in self._inventory_cache:
443
 
            self._inventory_cache[revid] = (
444
 
                self._branch.repository.get_revision_inventory(revid))
445
 
        return self._inventory_cache[revid]
 
553
        return self._branch.repository.get_revision_inventory(revid)
446
554
 
 
555
    @with_branch_lock
447
556
    def get_path(self, revid, file_id):
448
557
        if (file_id is None) or (file_id == ''):
449
558
            return ''
450
 
        path = self.get_inventory(revid).id2path(file_id)
 
559
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
451
560
        if (len(path) > 0) and not path.startswith('/'):
452
561
            path = '/' + path
453
562
        return path
454
563
 
 
564
    @with_branch_lock
455
565
    def get_file_id(self, revid, path):
456
566
        if (len(path) > 0) and not path.startswith('/'):
457
567
            path = '/' + path
458
 
        return self.get_inventory(revid).path2id(path)
 
568
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
 
569
 
459
570
 
460
571
    def get_merge_point_list(self, revid):
461
572
        """
495
606
            revnol = revno.split(".")
496
607
            revnos = ".".join(revnol[:-2])
497
608
            revnolast = int(revnol[-1])
498
 
            if revnos in d.keys():
 
609
            if d.has_key(revnos):
499
610
                m = d[revnos][0]
500
611
                if revnolast < m:
501
 
                    d[revnos] = (revnolast, revid)
 
612
                    d[revnos] = ( revnolast, revid )
502
613
            else:
503
 
                d[revnos] = (revnolast, revid)
504
 
 
505
 
        return [d[revnos][1] for revnos in d.keys()]
506
 
 
507
 
    def add_branch_nicks(self, change):
 
614
                d[revnos] = ( revnolast, revid )
 
615
 
 
616
        return [ d[revnos][1] for revnos in d.keys() ]
 
617
 
 
618
    def get_branch_nicks(self, changes):
508
619
        """
509
 
        given a 'change', fill in the branch nicks on all parents and merge
510
 
        points.
 
620
        given a list of changes from L{get_changes}, fill in the branch nicks
 
621
        on all parents and merge points.
511
622
        """
512
623
        fetch_set = set()
513
 
        for p in change.parents:
514
 
            fetch_set.add(p.revid)
515
 
        for p in change.merge_points:
516
 
            fetch_set.add(p.revid)
 
624
        for change in changes:
 
625
            for p in change.parents:
 
626
                fetch_set.add(p.revid)
 
627
            for p in change.merge_points:
 
628
                fetch_set.add(p.revid)
517
629
        p_changes = self.get_changes(list(fetch_set))
518
630
        p_change_dict = dict([(c.revid, c) for c in p_changes])
519
 
        for p in change.parents:
520
 
            if p.revid in p_change_dict:
521
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
522
 
            else:
523
 
                p.branch_nick = '(missing)'
524
 
        for p in change.merge_points:
525
 
            if p.revid in p_change_dict:
526
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
527
 
            else:
528
 
                p.branch_nick = '(missing)'
 
631
        for change in changes:
 
632
            # arch-converted branches may not have merged branch info :(
 
633
            for p in change.parents:
 
634
                if p.revid in p_change_dict:
 
635
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
636
                else:
 
637
                    p.branch_nick = '(missing)'
 
638
            for p in change.merge_points:
 
639
                if p.revid in p_change_dict:
 
640
                    p.branch_nick = p_change_dict[p.revid].branch_nick
 
641
                else:
 
642
                    p.branch_nick = '(missing)'
529
643
 
 
644
    @with_branch_lock
530
645
    def get_changes(self, revid_list):
531
646
        """Return a list of changes objects for the given revids.
532
647
 
533
648
        Revisions not present and NULL_REVISION will be ignored.
534
649
        """
535
 
        changes = self.get_changes_uncached(revid_list)
 
650
        if self._change_cache is None:
 
651
            changes = self.get_changes_uncached(revid_list)
 
652
        else:
 
653
            changes = self._change_cache.get_changes(revid_list)
536
654
        if len(changes) == 0:
537
655
            return changes
538
656
 
539
657
        # some data needs to be recalculated each time, because it may
540
658
        # change as new revisions are added.
541
659
        for change in changes:
542
 
            merge_revids = self.simplify_merge_point_list(
543
 
                               self.get_merge_point_list(change.revid))
544
 
            change.merge_points = [
545
 
                util.Container(revid=r,
546
 
                revno=self.get_revno(r)) for r in merge_revids]
 
660
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
 
661
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
547
662
            if len(change.parents) > 0:
548
 
                change.parents = [util.Container(revid=r,
549
 
                    revno=self.get_revno(r)) for r in change.parents]
 
663
                if isinstance(change.parents[0], util.Container):
 
664
                    # old cache stored a potentially-bogus revno
 
665
                    change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
 
666
                else:
 
667
                    change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
550
668
            change.revno = self.get_revno(change.revid)
551
669
 
552
670
        parity = 0
556
674
 
557
675
        return changes
558
676
 
 
677
    # alright, let's profile this sucka. (FIXME remove this eventually...)
 
678
    def _get_changes_profiled(self, revid_list):
 
679
        from loggerhead.lsprof import profile
 
680
        import cPickle
 
681
        ret, stats = profile(self.get_changes_uncached, revid_list)
 
682
        stats.sort()
 
683
        stats.freeze()
 
684
        cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
 
685
        self.log.info('lsprof complete!')
 
686
        return ret
 
687
 
 
688
    @with_branch_lock
 
689
    @with_bzrlib_read_lock
559
690
    def get_changes_uncached(self, revid_list):
560
 
        # FIXME: deprecated method in getting a null revision
561
691
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
562
692
                            revid_list)
563
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
564
 
                         revid_list)
 
693
        repo = self._branch.repository
 
694
        parent_map = repo.get_graph().get_parent_map(revid_list)
565
695
        # We need to return the answer in the same order as the input,
566
696
        # less any ghosts.
567
697
        present_revids = [revid for revid in revid_list
568
698
                          if revid in parent_map]
569
 
        rev_list = self._branch.repository.get_revisions(present_revids)
 
699
        rev_list = repo.get_revisions(present_revids)
570
700
 
571
701
        return [self._change_from_revision(rev) for rev in rev_list]
572
702
 
 
703
    def _get_deltas_for_revisions_with_trees(self, revisions):
 
704
        """Produce a list of revision deltas.
 
705
 
 
706
        Note that the input is a sequence of REVISIONS, not revision_ids.
 
707
        Trees will be held in memory until the generator exits.
 
708
        Each delta is relative to the revision's lefthand predecessor.
 
709
        (This is copied from bzrlib.)
 
710
        """
 
711
        required_trees = set()
 
712
        for revision in revisions:
 
713
            required_trees.add(revision.revid)
 
714
            required_trees.update([p.revid for p in revision.parents[:1]])
 
715
        trees = dict((t.get_revision_id(), t) for
 
716
                     t in self._branch.repository.revision_trees(required_trees))
 
717
        ret = []
 
718
        self._branch.repository.lock_read()
 
719
        try:
 
720
            for revision in revisions:
 
721
                if not revision.parents:
 
722
                    old_tree = self._branch.repository.revision_tree(
 
723
                        bzrlib.revision.NULL_REVISION)
 
724
                else:
 
725
                    old_tree = trees[revision.parents[0].revid]
 
726
                tree = trees[revision.revid]
 
727
                ret.append(tree.changes_from(old_tree))
 
728
            return ret
 
729
        finally:
 
730
            self._branch.repository.unlock()
 
731
 
573
732
    def _change_from_revision(self, revision):
574
733
        """
575
734
        Given a bzrlib Revision, return a processed "change" for use in
577
736
        """
578
737
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
579
738
 
580
 
        parents = [util.Container(revid=r,
581
 
                   revno=self.get_revno(r)) for r in revision.parent_ids]
 
739
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
582
740
 
583
741
        message, short_message = clean_message(revision.message)
584
742
 
585
 
        try:
586
 
            authors = revision.get_apparent_authors()
587
 
        except AttributeError:
588
 
            authors = [revision.get_apparent_author()]
589
 
 
590
743
        entry = {
591
744
            'revid': revision.revision_id,
592
745
            'date': commit_time,
593
 
            'authors': authors,
 
746
            'author': revision.committer,
594
747
            'branch_nick': revision.properties.get('branch-nick', None),
595
748
            'short_comment': short_message,
596
749
            'comment': revision.message,
599
752
        }
600
753
        return util.Container(entry)
601
754
 
602
 
    def get_file_changes_uncached(self, entry):
603
 
        repo = self._branch.repository
604
 
        if entry.parents:
605
 
            old_revid = entry.parents[0].revid
606
 
        else:
607
 
            old_revid = bzrlib.revision.NULL_REVISION
608
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
609
 
 
610
 
    def get_file_changes(self, entry):
 
755
    def get_file_changes_uncached(self, entries):
 
756
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
 
757
 
 
758
        return [self.parse_delta(delta) for delta in delta_list]
 
759
 
 
760
    @with_branch_lock
 
761
    def get_file_changes(self, entries):
611
762
        if self._file_change_cache is None:
612
 
            return self.get_file_changes_uncached(entry)
 
763
            return self.get_file_changes_uncached(entries)
613
764
        else:
614
 
            return self._file_change_cache.get_file_changes(entry)
615
 
 
616
 
    def add_changes(self, entry):
617
 
        changes = self.get_file_changes(entry)
618
 
        entry.changes = changes
619
 
 
 
765
            return self._file_change_cache.get_file_changes(entries)
 
766
 
 
767
    def add_changes(self, entries):
 
768
        changes_list = self.get_file_changes(entries)
 
769
 
 
770
        for entry, changes in zip(entries, changes_list):
 
771
            entry.changes = changes
 
772
 
 
773
    @with_branch_lock
 
774
    def get_change_with_diff(self, revid, compare_revid=None):
 
775
        change = self.get_changes([revid])[0]
 
776
 
 
777
        if compare_revid is None:
 
778
            if change.parents:
 
779
                compare_revid = change.parents[0].revid
 
780
            else:
 
781
                compare_revid = 'null:'
 
782
 
 
783
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
 
784
        rev_tree2 = self._branch.repository.revision_tree(revid)
 
785
        delta = rev_tree2.changes_from(rev_tree1)
 
786
 
 
787
        change.changes = self.parse_delta(delta)
 
788
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
 
789
 
 
790
        return change
 
791
 
 
792
    @with_branch_lock
620
793
    def get_file(self, file_id, revid):
621
794
        "returns (path, filename, data)"
622
795
        inv = self.get_inventory(revid)
627
800
            path = '/' + path
628
801
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
629
802
 
630
 
    def file_changes_for_revision_ids(self, old_revid, new_revid):
 
803
    def _parse_diffs(self, old_tree, new_tree, delta):
 
804
        """
 
805
        Return a list of processed diffs, in the format::
 
806
 
 
807
            list(
 
808
                filename: str,
 
809
                file_id: str,
 
810
                chunks: list(
 
811
                    diff: list(
 
812
                        old_lineno: int,
 
813
                        new_lineno: int,
 
814
                        type: str('context', 'delete', or 'insert'),
 
815
                        line: str,
 
816
                    ),
 
817
                ),
 
818
            )
 
819
        """
 
820
        process = []
 
821
        out = []
 
822
 
 
823
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
824
            if text_modified:
 
825
                process.append((old_path, new_path, fid, kind))
 
826
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
827
            process.append((path, path, fid, kind))
 
828
 
 
829
        for old_path, new_path, fid, kind in process:
 
830
            old_lines = old_tree.get_file_lines(fid)
 
831
            new_lines = new_tree.get_file_lines(fid)
 
832
            buffer = StringIO()
 
833
            if old_lines != new_lines:
 
834
                try:
 
835
                    bzrlib.diff.internal_diff(old_path, old_lines,
 
836
                                              new_path, new_lines, buffer)
 
837
                except bzrlib.errors.BinaryFile:
 
838
                    diff = ''
 
839
                else:
 
840
                    diff = buffer.getvalue()
 
841
            else:
 
842
                diff = ''
 
843
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
 
844
 
 
845
        return out
 
846
 
 
847
    def _process_diff(self, diff):
 
848
        # doesn't really need to be a method; could be static.
 
849
        chunks = []
 
850
        chunk = None
 
851
        for line in diff.splitlines():
 
852
            if len(line) == 0:
 
853
                continue
 
854
            if line.startswith('+++ ') or line.startswith('--- '):
 
855
                continue
 
856
            if line.startswith('@@ '):
 
857
                # new chunk
 
858
                if chunk is not None:
 
859
                    chunks.append(chunk)
 
860
                chunk = util.Container()
 
861
                chunk.diff = []
 
862
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
 
863
                old_lineno = lines[0]
 
864
                new_lineno = lines[1]
 
865
            elif line.startswith(' '):
 
866
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
 
867
                                                 type='context', line=util.fixed_width(line[1:])))
 
868
                old_lineno += 1
 
869
                new_lineno += 1
 
870
            elif line.startswith('+'):
 
871
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
 
872
                                                 type='insert', line=util.fixed_width(line[1:])))
 
873
                new_lineno += 1
 
874
            elif line.startswith('-'):
 
875
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
 
876
                                                 type='delete', line=util.fixed_width(line[1:])))
 
877
                old_lineno += 1
 
878
            else:
 
879
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
 
880
                                                 type='unknown', line=util.fixed_width(repr(line))))
 
881
        if chunk is not None:
 
882
            chunks.append(chunk)
 
883
        return chunks
 
884
 
 
885
    def parse_delta(self, delta):
631
886
        """
632
887
        Return a nested data structure containing the changes in a delta::
633
888
 
637
892
            modified: list(
638
893
                filename: str,
639
894
                file_id: str,
640
 
            ),
641
 
            text_changes: list((filename, file_id)),
642
 
        """
643
 
        repo = self._branch.repository
644
 
        if bzrlib.revision.is_null(old_revid) or \
645
 
               bzrlib.revision.is_null(new_revid):
646
 
            old_tree, new_tree = map(
647
 
                repo.revision_tree, [old_revid, new_revid])
648
 
        else:
649
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
650
 
 
651
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
652
 
 
653
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
654
 
 
655
 
        return util.Container(
656
 
            added=sorted(reporter.added, key=lambda x:x.filename),
657
 
            renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
658
 
            removed=sorted(reporter.removed, key=lambda x:x.filename),
659
 
            modified=sorted(reporter.modified, key=lambda x:x.filename),
660
 
            text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))
 
895
            )
 
896
        """
 
897
        added = []
 
898
        modified = []
 
899
        renamed = []
 
900
        removed = []
 
901
 
 
902
        for path, fid, kind in delta.added:
 
903
            added.append((rich_filename(path, kind), fid))
 
904
 
 
905
        for path, fid, kind, text_modified, meta_modified in delta.modified:
 
906
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
 
907
 
 
908
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
 
909
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
 
910
            if meta_modified or text_modified:
 
911
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
 
912
 
 
913
        for path, fid, kind in delta.removed:
 
914
            removed.append((rich_filename(path, kind), fid))
 
915
 
 
916
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
 
917
 
 
918
    @staticmethod
 
919
    def add_side_by_side(changes):
 
920
        # FIXME: this is a rotten API.
 
921
        for change in changes:
 
922
            for m in change.changes.modified:
 
923
                m.sbs_chunks = _make_side_by_side(m.chunks)
 
924
 
 
925
    @with_branch_lock
 
926
    def get_filelist(self, inv, file_id, sort_type=None):
 
927
        """
 
928
        return the list of all files (and their attributes) within a given
 
929
        path subtree.
 
930
        """
 
931
 
 
932
        dir_ie = inv[file_id]
 
933
        path = inv.id2path(file_id)
 
934
        file_list = []
 
935
 
 
936
        revid_set = set()
 
937
 
 
938
        for filename, entry in dir_ie.children.iteritems():
 
939
            revid_set.add(entry.revision)
 
940
 
 
941
        change_dict = {}
 
942
        for change in self.get_changes(list(revid_set)):
 
943
            change_dict[change.revid] = change
 
944
 
 
945
        for filename, entry in dir_ie.children.iteritems():
 
946
            pathname = filename
 
947
            if entry.kind == 'directory':
 
948
                pathname += '/'
 
949
 
 
950
            revid = entry.revision
 
951
 
 
952
            file = util.Container(
 
953
                filename=filename, executable=entry.executable, kind=entry.kind,
 
954
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
 
955
                revid=revid, change=change_dict[revid])
 
956
            file_list.append(file)
 
957
 
 
958
        if sort_type == 'filename' or sort_type is None:
 
959
            file_list.sort(key=lambda x: x.filename)
 
960
        elif sort_type == 'size':
 
961
            file_list.sort(key=lambda x: x.size)
 
962
        elif sort_type == 'date':
 
963
            file_list.sort(key=lambda x: x.change.date)
 
964
 
 
965
        parity = 0
 
966
        for file in file_list:
 
967
            file.parity = parity
 
968
            parity ^= 1
 
969
 
 
970
        return file_list
 
971
 
 
972
 
 
973
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
 
974
 
 
975
    @with_branch_lock
 
976
    def annotate_file(self, file_id, revid):
 
977
        z = time.time()
 
978
        lineno = 1
 
979
        parity = 0
 
980
 
 
981
        file_revid = self.get_inventory(revid)[file_id].revision
 
982
        oldvalues = None
 
983
        tree = self._branch.repository.revision_tree(file_revid)
 
984
        revid_set = set()
 
985
 
 
986
        for line_revid, text in tree.annotate_iter(file_id):
 
987
            revid_set.add(line_revid)
 
988
            if self._BADCHARS_RE.match(text):
 
989
                # bail out; this isn't displayable text
 
990
                yield util.Container(parity=0, lineno=1, status='same',
 
991
                                     text='(This is a binary file.)',
 
992
                                     change=util.Container())
 
993
                return
 
994
        change_cache = dict([(c.revid, c) \
 
995
                for c in self.get_changes(list(revid_set))])
 
996
 
 
997
        last_line_revid = None
 
998
        for line_revid, text in tree.annotate_iter(file_id):
 
999
            if line_revid == last_line_revid:
 
1000
                # remember which lines have a new revno and which don't
 
1001
                status = 'same'
 
1002
            else:
 
1003
                status = 'changed'
 
1004
                parity ^= 1
 
1005
                last_line_revid = line_revid
 
1006
                change = change_cache[line_revid]
 
1007
                trunc_revno = change.revno
 
1008
                if len(trunc_revno) > 10:
 
1009
                    trunc_revno = trunc_revno[:9] + '...'
 
1010
 
 
1011
            yield util.Container(parity=parity, lineno=lineno, status=status,
 
1012
                                 change=change, text=util.fixed_width(text))
 
1013
            lineno += 1
 
1014
 
 
1015
        self.log.debug('annotate: %r secs' % (time.time() - z,))
 
1016
 
 
1017
    @with_branch_lock
 
1018
    def get_bundle(self, revid, compare_revid=None):
 
1019
        if compare_revid is None:
 
1020
            parents = self._revision_graph[revid]
 
1021
            if len(parents) > 0:
 
1022
                compare_revid = parents[0]
 
1023
            else:
 
1024
                compare_revid = None
 
1025
        s = StringIO()
 
1026
        bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
 
1027
        return s.getvalue()