~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Francesco 'pr0gg3d' Del Degan
  • Date: 2011-08-08 20:58:09 UTC
  • mto: This revision was merged to the branch mainline in revision 454.
  • Revision ID: f.deldegan@pr0gg3d.net-20110808205809-n3eg73n80ymyd6kj
Fixes bug in annotate that occurs when file is zero-sized

Show diffs side-by-side

added added

removed removed

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