~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
 
# Copyright (C) 2006-2011 Canonical Ltd.
 
1
#
 
2
# Copyright (C) 2008  Canonical Ltd. 
2
3
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
3
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
4
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
33
34
import re
34
35
import textwrap
35
36
import threading
36
 
import tarfile
37
 
 
38
 
from bzrlib import tag
 
37
import time
 
38
from StringIO import StringIO
 
39
 
 
40
from loggerhead import search
 
41
from loggerhead import util
 
42
from loggerhead.wholehistory import compute_whole_history_data
 
43
 
 
44
import bzrlib
39
45
import bzrlib.branch
40
 
import bzrlib.delta
 
46
import bzrlib.diff
41
47
import bzrlib.errors
42
 
import bzrlib.foreign
 
48
import bzrlib.progress
43
49
import bzrlib.revision
44
 
 
45
 
from loggerhead import search
46
 
from loggerhead import util
47
 
from loggerhead.wholehistory import compute_whole_history_data
48
 
from bzrlib.export.tar_exporter import export_tarball
 
50
import bzrlib.tsort
 
51
import bzrlib.ui
 
52
 
 
53
# bzrlib's UIFactory is not thread-safe
 
54
uihack = threading.local()
 
55
 
 
56
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
 
57
    def nested_progress_bar(self):
 
58
        if getattr(uihack, '_progress_bar_stack', None) is None:
 
59
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
 
60
        return uihack._progress_bar_stack.get_nested()
 
61
 
 
62
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
 
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
49
123
 
50
124
 
51
125
def is_branch(folder):
63
137
    module (Robey, the original author of this code, apparently favored this
64
138
    style of message).
65
139
    """
66
 
    message = message.lstrip().splitlines()
 
140
    message = message.splitlines()
67
141
 
68
142
    if len(message) == 1:
69
143
        message = textwrap.wrap(message[0])
90
164
    return path
91
165
 
92
166
 
 
167
 
 
168
# from bzrlib
93
169
class _RevListToTimestamps(object):
94
170
    """This takes a list of revisions, and allows you to bisect by date"""
95
171
 
101
177
 
102
178
    def __getitem__(self, index):
103
179
        """Get the date of the index'd item"""
104
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
105
 
                   self.revid_list[index]).timestamp)
 
180
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
106
181
 
107
182
    def __len__(self):
108
183
        return len(self.revid_list)
109
184
 
110
 
class FileChangeReporter(object):
111
 
 
112
 
    def __init__(self, old_inv, new_inv):
113
 
        self.added = []
114
 
        self.modified = []
115
 
        self.renamed = []
116
 
        self.removed = []
117
 
        self.text_changes = []
118
 
        self.old_inv = old_inv
119
 
        self.new_inv = new_inv
120
 
 
121
 
    def revid(self, inv, file_id):
122
 
        try:
123
 
            return inv[file_id].revision
124
 
        except bzrlib.errors.NoSuchId:
125
 
            return 'null:'
126
 
 
127
 
    def report(self, file_id, paths, versioned, renamed, modified,
128
 
               exe_change, kind):
129
 
        if modified not in ('unchanged', 'kind changed'):
130
 
            if versioned == 'removed':
131
 
                filename = rich_filename(paths[0], kind[0])
132
 
            else:
133
 
                filename = rich_filename(paths[1], kind[1])
134
 
            self.text_changes.append(util.Container(
135
 
                filename=filename, file_id=file_id,
136
 
                old_revision=self.revid(self.old_inv, file_id),
137
 
                new_revision=self.revid(self.new_inv, file_id)))
138
 
        if versioned == 'added':
139
 
            self.added.append(util.Container(
140
 
                filename=rich_filename(paths[1], kind),
141
 
                file_id=file_id, kind=kind[1]))
142
 
        elif versioned == 'removed':
143
 
            self.removed.append(util.Container(
144
 
                filename=rich_filename(paths[0], kind),
145
 
                file_id=file_id, kind=kind[0]))
146
 
        elif renamed:
147
 
            self.renamed.append(util.Container(
148
 
                old_filename=rich_filename(paths[0], kind[0]),
149
 
                new_filename=rich_filename(paths[1], kind[1]),
150
 
                file_id=file_id,
151
 
                text_modified=modified == 'modified'))
152
 
        else:
153
 
            self.modified.append(util.Container(
154
 
                filename=rich_filename(paths[1], kind),
155
 
                file_id=file_id))
156
 
 
157
 
# The lru_cache is not thread-safe, so we need a lock around it for
158
 
# all threads.
159
 
rev_info_memory_cache_lock = threading.RLock()
160
 
 
161
 
class RevInfoMemoryCache(object):
162
 
    """A store that validates values against the revids they were stored with.
163
 
 
164
 
    We use a unique key for each branch.
165
 
 
166
 
    The reason for not just using the revid as the key is so that when a new
167
 
    value is provided for a branch, we replace the old value used for the
168
 
    branch.
169
 
 
170
 
    There is another implementation of the same interface in
171
 
    loggerhead.changecache.RevInfoDiskCache.
172
 
    """
173
 
 
174
 
    def __init__(self, cache):
175
 
        self._cache = cache
176
 
 
177
 
    def get(self, key, revid):
178
 
        """Return the data associated with `key`, subject to a revid check.
179
 
 
180
 
        If a value was stored under `key`, with the same revid, return it.
181
 
        Otherwise return None.
182
 
        """
183
 
        rev_info_memory_cache_lock.acquire()
184
 
        try:
185
 
            cached = self._cache.get(key)
186
 
        finally:
187
 
            rev_info_memory_cache_lock.release()
188
 
        if cached is None:
189
 
            return None
190
 
        stored_revid, data = cached
191
 
        if revid == stored_revid:
192
 
            return data
193
 
        else:
194
 
            return None
195
 
 
196
 
    def set(self, key, revid, data):
197
 
        """Store `data` under `key`, to be checked against `revid` on get().
198
 
        """
199
 
        rev_info_memory_cache_lock.acquire()
200
 
        try:
201
 
            self._cache[key] = (revid, data)
202
 
        finally:
203
 
            rev_info_memory_cache_lock.release()
204
 
 
205
 
# Used to store locks that prevent multiple threads from building a 
206
 
# revision graph for the same branch at the same time, because that can
207
 
# cause severe performance issues that are so bad that the system seems
208
 
# to hang.
209
 
revision_graph_locks = {}
210
 
revision_graph_check_lock = threading.Lock()
211
 
 
212
 
class History(object):
 
185
 
 
186
class History (object):
213
187
    """Decorate a branch to provide information for rendering.
214
188
 
215
189
    History objects are expected to be short lived -- when serving a request
217
191
    around it, serve the request, throw the History object away, unlock the
218
192
    branch and throw it away.
219
193
 
220
 
    :ivar _file_change_cache: An object that caches information about the
221
 
        files that changed between two revisions.
222
 
    :ivar _rev_info: A list of information about revisions.  This is by far
223
 
        the most cryptic data structure in loggerhead.  At the top level, it
224
 
        is a list of 3-tuples [(merge-info, where-merged, parents)].
225
 
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
226
 
        like a merged sorted list, but the revno is stringified.
227
 
        `where-merged` is a tuple of revisions that have this revision as a
228
 
        non-lefthand parent.  Finally, `parents` is just the usual list of
229
 
        parents of this revision.
230
 
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
231
 
        the information about it in _rev_info.
232
 
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
233
 
        ids.
 
194
    :ivar _file_change_cache: xx
234
195
    """
235
196
 
236
 
    def _load_whole_history_data(self, caches, cache_key):
237
 
        """Set the attributes relating to the whole history of the branch.
238
 
 
239
 
        :param caches: a list of caches with interfaces like
240
 
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
241
 
        :param cache_key: the key to use with the caches.
242
 
        """
243
 
        self._rev_indices = None
244
 
        self._rev_info = None
245
 
 
246
 
        missed_caches = []
247
 
        def update_missed_caches():
248
 
            for cache in missed_caches:
249
 
                cache.set(cache_key, self.last_revid, self._rev_info)
250
 
 
251
 
        # Theoretically, it's possible for two threads to race in creating
252
 
        # the Lock() object for their branch, so we put a lock around
253
 
        # creating the per-branch Lock().
254
 
        revision_graph_check_lock.acquire()
255
 
        try:
256
 
            if cache_key not in revision_graph_locks:
257
 
                revision_graph_locks[cache_key] = threading.Lock()
258
 
        finally:
259
 
            revision_graph_check_lock.release()
260
 
 
261
 
        revision_graph_locks[cache_key].acquire()
262
 
        try:
263
 
            for cache in caches:
264
 
                data = cache.get(cache_key, self.last_revid)
265
 
                if data is not None:
266
 
                    self._rev_info = data
267
 
                    update_missed_caches()
268
 
                    break
269
 
                else:
270
 
                    missed_caches.append(cache)
271
 
            else:
272
 
                whole_history_data = compute_whole_history_data(self._branch)
273
 
                self._rev_info, self._rev_indices = whole_history_data
274
 
                update_missed_caches()
275
 
        finally:
276
 
            revision_graph_locks[cache_key].release()
277
 
 
278
 
        if self._rev_indices is not None:
279
 
            self._revno_revid = {}
280
 
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
281
 
                self._revno_revid[revno_str] = revid
282
 
        else:
283
 
            self._revno_revid = {}
284
 
            self._rev_indices = {}
285
 
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
286
 
                self._rev_indices[revid] = seq
287
 
                self._revno_revid[revno_str] = revid
288
 
 
289
 
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
290
 
                 revinfo_disk_cache=None, cache_key=None):
 
197
    def __init__(self, branch, whole_history_data_cache):
291
198
        assert branch.is_locked(), (
292
199
            "Can only construct a History object with a read-locked branch.")
293
 
        if file_cache is not None:
294
 
            self._file_change_cache = file_cache
295
 
            file_cache.history = self
296
 
        else:
297
 
            self._file_change_cache = None
 
200
        self._file_change_cache = None
298
201
        self._branch = branch
299
 
        self._branch_tags = None
300
202
        self._inventory_cache = {}
301
 
        self._branch_nick = self._branch.get_config().get_nickname()
302
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
 
203
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
303
204
 
304
205
        self.last_revid = branch.last_revision()
305
206
 
306
 
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
307
 
        if revinfo_disk_cache:
308
 
            caches.append(revinfo_disk_cache)
309
 
        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
310
219
 
311
220
    @property
312
221
    def has_revisions(self):
316
225
        return self._branch.get_config()
317
226
 
318
227
    def get_revno(self, revid):
319
 
        if revid not in self._rev_indices:
 
228
        if revid not in self._revision_info:
320
229
            # ghost parent?
321
230
            return 'unknown'
322
 
        seq = self._rev_indices[revid]
323
 
        revno = self._rev_info[seq][0][3]
324
 
        return revno
 
231
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
232
        return revno_str
325
233
 
326
234
    def get_revids_from(self, revid_list, start_revid):
327
235
        """
329
237
        revid in revid_list.
330
238
        """
331
239
        if revid_list is None:
332
 
            # Just yield the mainline, starting at start_revid
333
 
            revid = start_revid
334
 
            is_null = bzrlib.revision.is_null
335
 
            while not is_null(revid):
336
 
                yield revid
337
 
                parents = self._rev_info[self._rev_indices[revid]][2]
338
 
                if not parents:
339
 
                    return
340
 
                revid = parents[0]
341
 
            return
 
240
            revid_list = self._full_history
342
241
        revid_set = set(revid_list)
343
242
        revid = start_revid
344
 
 
345
243
        def introduced_revisions(revid):
346
244
            r = set([revid])
347
 
            seq = self._rev_indices[revid]
348
 
            md = self._rev_info[seq][0][2]
 
245
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
349
246
            i = seq + 1
350
 
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
351
 
                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])
352
249
                i += 1
353
250
            return r
354
 
        while revid_set:
 
251
        while 1:
355
252
            if bzrlib.revision.is_null(revid):
356
253
                return
357
 
            rev_introduced = introduced_revisions(revid)
358
 
            matching = rev_introduced.intersection(revid_set)
359
 
            if matching:
360
 
                # We don't need to look for these anymore.
361
 
                revid_set.difference_update(matching)
 
254
            if introduced_revisions(revid) & revid_set:
362
255
                yield revid
363
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
256
            parents = self._revision_graph[revid]
364
257
            if len(parents) == 0:
365
258
                return
366
259
            revid = parents[0]
368
261
    def get_short_revision_history_by_fileid(self, file_id):
369
262
        # FIXME: would be awesome if we could get, for a folder, the list of
370
263
        # revisions where items within that folder changed.i
371
 
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
372
 
        get_parent_map = self._branch.repository.texts.get_parent_map
373
 
        # We chunk the requests as this works better with GraphIndex.
374
 
        # See _filter_revisions_touching_file_id in bzrlib/log.py
375
 
        # for more information.
376
 
        revids = []
377
 
        chunk_size = 1000
378
 
        for start in xrange(0, len(possible_keys), chunk_size):
379
 
            next_keys = possible_keys[start:start + chunk_size]
380
 
            revids += [k[1] for k in get_parent_map(next_keys)]
381
 
        del possible_keys, next_keys
 
264
        try:
 
265
            # FIXME: Workaround for bzr versions prior to 1.6b3. 
 
266
            # Remove me eventually pretty please  :)
 
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] 
 
270
        except AttributeError:
 
271
            possible_keys = [(file_id, revid) for revid in self._full_history]
 
272
            existing_keys = self._branch.repository.texts.get_parent_map(possible_keys)
 
273
            revids = [revid for _, revid in existing_keys.iterkeys()]
382
274
        return revids
383
275
 
384
276
    def get_revision_history_since(self, revid_list, date):
385
277
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
386
278
        # so start at midnight on 02-sep.
387
279
        date = date + datetime.timedelta(days=1)
388
 
        # our revid list is sorted in REVERSE date order,
389
 
        # so go thru some hoops here...
 
280
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
390
281
        revid_list.reverse()
391
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
392
 
                                                   self._branch.repository),
393
 
                              date)
 
282
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
394
283
        if index == 0:
395
284
            return []
396
285
        revid_list.reverse()
402
291
        given a "quick-search" query, try a few obvious possible meanings:
403
292
 
404
293
            - revision id or # ("128.1.3")
405
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
406
 
iso style "yyyy-mm-dd")
 
294
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
407
295
            - comment text as a fallback
408
296
 
409
297
        and return a revid list that matches.
412
300
        # all the relevant changes (time-consuming) only to return a list of
413
301
        # revids which will be used to fetch a set of changes again.
414
302
 
415
 
        # if they entered a revid, just jump straight there;
416
 
        # ignore the passed-in revid_list
 
303
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
417
304
        revid = self.fix_revid(query)
418
305
        if revid is not None:
419
306
            if isinstance(revid, unicode):
420
307
                revid = revid.encode('utf-8')
421
 
            changes = self.get_changes([revid])
 
308
            changes = self.get_changes([ revid ])
422
309
            if (changes is not None) and (len(changes) > 0):
423
 
                return [revid]
 
310
                return [ revid ]
424
311
 
425
312
        date = None
426
313
        m = self.us_date_re.match(query)
427
314
        if m is not None:
428
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
429
 
                                     int(m.group(1)),
430
 
                                     int(m.group(2)))
 
315
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
431
316
        else:
432
317
            m = self.earth_date_re.match(query)
433
318
            if m is not None:
434
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
435
 
                                         int(m.group(2)),
436
 
                                         int(m.group(1)))
 
319
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
437
320
            else:
438
321
                m = self.iso_date_re.match(query)
439
322
                if m is not None:
440
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
441
 
                                             int(m.group(2)),
442
 
                                             int(m.group(3)))
 
323
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
443
324
        if date is not None:
444
325
            if revid_list is None:
445
 
                # if no limit to the query was given,
446
 
                # search only the direct-parent path.
 
326
                # if no limit to the query was given, search only the direct-parent path.
447
327
                revid_list = list(self.get_revids_from(None, self.last_revid))
448
328
            return self.get_revision_history_since(revid_list, date)
449
329
 
461
341
            return revid
462
342
        if revid == 'head:':
463
343
            return self.last_revid
464
 
        try:
465
 
            if self.revno_re.match(revid):
466
 
                revid = self._revno_revid[revid]
467
 
        except KeyError:
468
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
344
        if self.revno_re.match(revid):
 
345
            revid = self._revno_revid[revid]
469
346
        return revid
470
347
 
471
348
    def get_file_view(self, revid, file_id):
479
356
        if revid is None:
480
357
            revid = self.last_revid
481
358
        if file_id is not None:
482
 
            revlist = list(
483
 
                self.get_short_revision_history_by_fileid(file_id))
484
 
            revlist = self.get_revids_from(revlist, revid)
 
359
            # since revid is 'start_revid', possibly should start the path
 
360
            # tracing from revid... FIXME
 
361
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
 
362
            revlist = list(self.get_revids_from(revlist, revid))
485
363
        else:
486
 
            revlist = self.get_revids_from(None, revid)
 
364
            revlist = list(self.get_revids_from(None, revid))
487
365
        return revlist
488
366
 
489
 
    @staticmethod
490
 
    def _iterate_sufficiently(iterable, stop_at, extra_rev_count):
491
 
        """Return a list of iterable.
492
 
 
493
 
        If extra_rev_count is None, fully consume iterable.
494
 
        Otherwise, stop at 'stop_at' + extra_rev_count.
495
 
 
496
 
        Example:
497
 
          iterate until you find stop_at, then iterate 10 more times.
498
 
        """
499
 
        if extra_rev_count is None:
500
 
            return list(iterable)
501
 
        result = []
502
 
        found = False
503
 
        for n in iterable:
504
 
            result.append(n)
505
 
            if n == stop_at:
506
 
                found = True
507
 
                break
508
 
        if found:
509
 
            for count, n in enumerate(iterable):
510
 
                if count >= extra_rev_count:
511
 
                    break
512
 
                result.append(n)
513
 
        return result
514
 
 
515
 
    def get_view(self, revid, start_revid, file_id, query=None,
516
 
                 extra_rev_count=None):
 
367
    def get_view(self, revid, start_revid, file_id, query=None):
517
368
        """
518
369
        use the URL parameters (revid, start_revid, file_id, and query) to
519
370
        determine the revision list we're viewing (start_revid, file_id, query)
524
375
              file.
525
376
            - if a start_revid is given, we're viewing the branch from a
526
377
              specific revision up the tree.
527
 
            - if extra_rev_count is given, find the view from start_revid =>
528
 
              revid, and continue an additional 'extra_rev_count'. If not
529
 
              given, then revid_list will contain the full history of
530
 
              start_revid
531
378
 
532
379
        these may be combined to view revisions for a specific file, from
533
380
        a specific revision, with a specific search query.
546
393
 
547
394
        if query is None:
548
395
            revid_list = self.get_file_view(start_revid, file_id)
549
 
            revid_list = self._iterate_sufficiently(revid_list, revid,
550
 
                                                    extra_rev_count)
551
396
            if revid is None:
552
397
                revid = start_revid
553
398
            if revid not in revid_list:
554
399
                # if the given revid is not in the revlist, use a revlist that
555
400
                # starts at the given revid.
556
401
                revid_list = self.get_file_view(revid, file_id)
557
 
                revid_list = self._iterate_sufficiently(revid_list, revid,
558
 
                                                        extra_rev_count)
559
402
                start_revid = revid
560
403
            return revid, start_revid, revid_list
561
404
 
578
421
    def get_inventory(self, revid):
579
422
        if revid not in self._inventory_cache:
580
423
            self._inventory_cache[revid] = (
581
 
                self._branch.repository.get_inventory(revid))
 
424
                self._branch.repository.get_revision_inventory(revid))
582
425
        return self._inventory_cache[revid]
583
426
 
584
427
    def get_path(self, revid, file_id):
603
446
 
604
447
        merge_point = []
605
448
        while True:
606
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
449
            children = self._where_merged.get(revid, [])
607
450
            nexts = []
608
451
            for child in children:
609
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
452
                child_parents = self._revision_graph[child]
610
453
                if child_parents[0] == revid:
611
454
                    nexts.append(child)
612
455
                else:
632
475
            revnol = revno.split(".")
633
476
            revnos = ".".join(revnol[:-2])
634
477
            revnolast = int(revnol[-1])
635
 
            if revnos in d:
 
478
            if d.has_key(revnos):
636
479
                m = d[revnos][0]
637
480
                if revnolast < m:
638
 
                    d[revnos] = (revnolast, revid)
 
481
                    d[revnos] = ( revnolast, revid )
639
482
            else:
640
 
                d[revnos] = (revnolast, revid)
641
 
 
642
 
        return [revid for (_, revid) in d.itervalues()]
643
 
 
644
 
    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):
645
488
        """
646
 
        given a 'change', fill in the branch nicks on all parents and merge
647
 
        points.
 
489
        given a list of changes from L{get_changes}, fill in the branch nicks
 
490
        on all parents and merge points.
648
491
        """
649
492
        fetch_set = set()
650
 
        for p in change.parents:
651
 
            fetch_set.add(p.revid)
652
 
        for p in change.merge_points:
653
 
            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)
654
498
        p_changes = self.get_changes(list(fetch_set))
655
499
        p_change_dict = dict([(c.revid, c) for c in p_changes])
656
 
        for p in change.parents:
657
 
            if p.revid in p_change_dict:
658
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
659
 
            else:
660
 
                p.branch_nick = '(missing)'
661
 
        for p in change.merge_points:
662
 
            if p.revid in p_change_dict:
663
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
664
 
            else:
665
 
                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)'
666
512
 
667
513
    def get_changes(self, revid_list):
668
514
        """Return a list of changes objects for the given revids.
676
522
        # some data needs to be recalculated each time, because it may
677
523
        # change as new revisions are added.
678
524
        for change in changes:
679
 
            merge_revids = self.simplify_merge_point_list(
680
 
                               self.get_merge_point_list(change.revid))
681
 
            change.merge_points = [
682
 
                util.Container(revid=r,
683
 
                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]
684
527
            if len(change.parents) > 0:
685
 
                change.parents = [util.Container(revid=r,
 
528
                change.parents = [util.Container(revid=r, 
686
529
                    revno=self.get_revno(r)) for r in change.parents]
687
530
            change.revno = self.get_revno(change.revid)
688
531
 
697
540
        # FIXME: deprecated method in getting a null revision
698
541
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
699
542
                            revid_list)
700
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
701
 
                         revid_list)
 
543
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
702
544
        # We need to return the answer in the same order as the input,
703
545
        # less any ghosts.
704
546
        present_revids = [revid for revid in revid_list
707
549
 
708
550
        return [self._change_from_revision(rev) for rev in rev_list]
709
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
 
710
577
    def _change_from_revision(self, revision):
711
578
        """
712
579
        Given a bzrlib Revision, return a processed "change" for use in
713
580
        templates.
714
581
        """
 
582
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
 
583
 
 
584
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
 
585
 
715
586
        message, short_message = clean_message(revision.message)
716
587
 
717
 
        if self._branch_tags is None:
718
 
            self._branch_tags = self._branch.tags.get_reverse_tag_dict()
719
 
 
720
 
        revtags = None
721
 
        if revision.revision_id in self._branch_tags:
722
 
          # tag.sort_* functions expect (tag, data) pairs, so we generate them,
723
 
          # and then strip them
724
 
          tags = [(t, None) for t in self._branch_tags[revision.revision_id]]
725
 
          sort_func = getattr(tag, 'sort_natural', None)
726
 
          if sort_func is None:
727
 
              tags.sort()
728
 
          else:
729
 
              sort_func(self._branch, tags)
730
 
          revtags = u', '.join([t[0] for t in tags])
731
 
 
732
588
        entry = {
733
589
            'revid': revision.revision_id,
734
 
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
735
 
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
736
 
            'committer': revision.committer,
737
 
            'authors': revision.get_apparent_authors(),
 
590
            'date': commit_time,
 
591
            'author': revision.get_apparent_author(),
738
592
            'branch_nick': revision.properties.get('branch-nick', None),
739
593
            'short_comment': short_message,
740
594
            'comment': revision.message,
741
595
            'comment_clean': [util.html_clean(s) for s in message],
742
596
            'parents': revision.parent_ids,
743
 
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
744
 
            'tags': revtags,
745
597
        }
746
 
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
747
 
            foreign_revid, mapping = (
748
 
                revision.foreign_revid, revision.mapping)
749
 
        elif ":" in revision.revision_id:
750
 
            try:
751
 
                foreign_revid, mapping = \
752
 
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
753
 
                        revision.revision_id)
754
 
            except bzrlib.errors.InvalidRevisionId:
755
 
                foreign_revid = None
756
 
                mapping = None
757
 
        else:
758
 
            foreign_revid = None
759
 
        if foreign_revid is not None:
760
 
            entry["foreign_vcs"] = mapping.vcs.abbreviation
761
 
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
762
598
        return util.Container(entry)
763
599
 
764
 
    def get_file_changes_uncached(self, entry):
765
 
        if entry.parents:
766
 
            old_revid = entry.parents[0].revid
767
 
        else:
768
 
            old_revid = bzrlib.revision.NULL_REVISION
769
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
770
 
 
771
 
    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):
772
606
        if self._file_change_cache is None:
773
 
            return self.get_file_changes_uncached(entry)
 
607
            return self.get_file_changes_uncached(entries)
774
608
        else:
775
 
            return self._file_change_cache.get_file_changes(entry)
776
 
 
777
 
    def add_changes(self, entry):
778
 
        changes = self.get_file_changes(entry)
779
 
        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
780
634
 
781
635
    def get_file(self, file_id, revid):
782
 
        """Returns (path, filename, file contents)"""
 
636
        "returns (path, filename, data)"
783
637
        inv = self.get_inventory(revid)
784
638
        inv_entry = inv[file_id]
785
639
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
788
642
            path = '/' + path
789
643
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
790
644
 
791
 
    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):
792
734
        """
793
735
        Return a nested data structure containing the changes in a delta::
794
736
 
798
740
            modified: list(
799
741
                filename: str,
800
742
                file_id: str,
801
 
            ),
802
 
            text_changes: list((filename, file_id)),
803
 
        """
804
 
        repo = self._branch.repository
805
 
        if (bzrlib.revision.is_null(old_revid) or
806
 
            bzrlib.revision.is_null(new_revid)):
807
 
            old_tree, new_tree = map(
808
 
                repo.revision_tree, [old_revid, new_revid])
809
 
        else:
810
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
811
 
 
812
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
813
 
 
814
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
815
 
 
816
 
        return util.Container(
817
 
            added=sorted(reporter.added, key=lambda x: x.filename),
818
 
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
819
 
            removed=sorted(reporter.removed, key=lambda x: x.filename),
820
 
            modified=sorted(reporter.modified, key=lambda x: x.filename),
821
 
            text_changes=sorted(reporter.text_changes,
822
 
                                key=lambda x: x.filename))
823
 
 
 
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,))