~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: John Arbash Meinel
  • Date: 2008-07-26 14:52:44 UTC
  • mto: This revision was merged to the branch mainline in revision 185.
  • Revision ID: john@arbash-meinel.com-20080726145244-l7h1ndtlu5mnm9tg
Add Copyright information to most files.

Fix the documentation for start/stop in the README.txt

Show diffs side-by-side

added added

removed removed

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