~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2008-06-22 21:15:58 UTC
  • Revision ID: michael.hudson@canonical.com-20080622211558-w8zafrhnampc5mz6
make using url_prefix not break, at least

Show diffs side-by-side

added added

removed removed

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