~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

Merge loggerhead trunk, to get the updated test suite

Show diffs side-by-side

added added

removed removed

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