~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

tidy up link generation

Show diffs side-by-side

added added

removed removed

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