~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Michael Hudson
  • Date: 2008-08-01 04:48:26 UTC
  • mfrom: (189.1.2 user-aware)
  • Revision ID: michael.hudson@canonical.com-20080801044826-3b1601affjg9uo9u
Merge Tim's changes to support the url traversal we want on bzr-playground.
On the way, clean up how paths get passed around loggerhead/apps/filesystem.py
and add the first hints of configuration for the serve-branches script.

Show diffs side-by-side

added added

removed removed

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