~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Martin Albisetti
  • Date: 2008-09-10 22:53:39 UTC
  • mfrom: (219.1.3 reloader)
  • Revision ID: argentina@gmail.com-20080910225339-p987y5hgtxrq8p5c
Add --reload option to restart LH automatically when developing. (Guillermo Gonzalez)

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