~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
1
#
2
 
# Copyright (C) 2008, 2009 Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
34
32
import re
35
33
import textwrap
36
34
import threading
37
 
 
 
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
38
43
import bzrlib.branch
39
 
import bzrlib.delta
 
44
import bzrlib.diff
40
45
import bzrlib.errors
41
 
import bzrlib.foreign
 
46
import bzrlib.progress
42
47
import bzrlib.revision
43
 
 
44
 
from loggerhead import search
45
 
from loggerhead import util
46
 
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
47
111
 
48
112
 
49
113
def is_branch(folder):
61
125
    module (Robey, the original author of this code, apparently favored this
62
126
    style of message).
63
127
    """
64
 
    message = message.lstrip().splitlines()
 
128
    message = message.splitlines()
65
129
 
66
130
    if len(message) == 1:
67
131
        message = textwrap.wrap(message[0])
88
152
    return path
89
153
 
90
154
 
 
155
 
 
156
# from bzrlib
91
157
class _RevListToTimestamps(object):
92
158
    """This takes a list of revisions, and allows you to bisect by date"""
93
159
 
99
165
 
100
166
    def __getitem__(self, index):
101
167
        """Get the date of the index'd item"""
102
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
103
 
                   self.revid_list[index]).timestamp)
 
168
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
104
169
 
105
170
    def __len__(self):
106
171
        return len(self.revid_list)
107
172
 
108
 
class FileChangeReporter(object):
109
 
 
110
 
    def __init__(self, old_inv, new_inv):
111
 
        self.added = []
112
 
        self.modified = []
113
 
        self.renamed = []
114
 
        self.removed = []
115
 
        self.text_changes = []
116
 
        self.old_inv = old_inv
117
 
        self.new_inv = new_inv
118
 
 
119
 
    def revid(self, inv, file_id):
120
 
        try:
121
 
            return inv[file_id].revision
122
 
        except bzrlib.errors.NoSuchId:
123
 
            return 'null:'
124
 
 
125
 
    def report(self, file_id, paths, versioned, renamed, modified,
126
 
               exe_change, kind):
127
 
        if modified not in ('unchanged', 'kind changed'):
128
 
            if versioned == 'removed':
129
 
                filename = rich_filename(paths[0], kind[0])
130
 
            else:
131
 
                filename = rich_filename(paths[1], kind[1])
132
 
            self.text_changes.append(util.Container(
133
 
                filename=filename, file_id=file_id,
134
 
                old_revision=self.revid(self.old_inv, file_id),
135
 
                new_revision=self.revid(self.new_inv, file_id)))
136
 
        if versioned == 'added':
137
 
            self.added.append(util.Container(
138
 
                filename=rich_filename(paths[1], kind),
139
 
                file_id=file_id, kind=kind[1]))
140
 
        elif versioned == 'removed':
141
 
            self.removed.append(util.Container(
142
 
                filename=rich_filename(paths[0], kind),
143
 
                file_id=file_id, kind=kind[0]))
144
 
        elif renamed:
145
 
            self.renamed.append(util.Container(
146
 
                old_filename=rich_filename(paths[0], kind[0]),
147
 
                new_filename=rich_filename(paths[1], kind[1]),
148
 
                file_id=file_id,
149
 
                text_modified=modified == 'modified'))
150
 
        else:
151
 
            self.modified.append(util.Container(
152
 
                filename=rich_filename(paths[1], kind),
153
 
                file_id=file_id))
154
 
 
155
 
# The lru_cache is not thread-safe, so we need a lock around it for
156
 
# all threads.
157
 
rev_info_memory_cache_lock = threading.RLock()
158
 
 
159
 
class RevInfoMemoryCache(object):
160
 
    """A store that validates values against the revids they were stored with.
161
 
 
162
 
    We use a unique key for each branch.
163
 
 
164
 
    The reason for not just using the revid as the key is so that when a new
165
 
    value is provided for a branch, we replace the old value used for the
166
 
    branch.
167
 
 
168
 
    There is another implementation of the same interface in
169
 
    loggerhead.changecache.RevInfoDiskCache.
170
 
    """
171
 
 
172
 
    def __init__(self, cache):
173
 
        self._cache = cache
174
 
 
175
 
    def get(self, key, revid):
176
 
        """Return the data associated with `key`, subject to a revid check.
177
 
 
178
 
        If a value was stored under `key`, with the same revid, return it.
179
 
        Otherwise return None.
180
 
        """
181
 
        rev_info_memory_cache_lock.acquire()
182
 
        try:
183
 
            cached = self._cache.get(key)
184
 
        finally:
185
 
            rev_info_memory_cache_lock.release()
186
 
        if cached is None:
187
 
            return None
188
 
        stored_revid, data = cached
189
 
        if revid == stored_revid:
190
 
            return data
191
 
        else:
192
 
            return None
193
 
 
194
 
    def set(self, key, revid, data):
195
 
        """Store `data` under `key`, to be checked against `revid` on get().
196
 
        """
197
 
        rev_info_memory_cache_lock.acquire()
198
 
        try:
199
 
            self._cache[key] = (revid, data)
200
 
        finally:
201
 
            rev_info_memory_cache_lock.release()
202
 
 
203
 
# Used to store locks that prevent multiple threads from building a 
204
 
# revision graph for the same branch at the same time, because that can
205
 
# cause severe performance issues that are so bad that the system seems
206
 
# to hang.
207
 
revision_graph_locks = {}
208
 
revision_graph_check_lock = threading.Lock()
209
 
 
210
 
class History(object):
 
173
 
 
174
class History (object):
211
175
    """Decorate a branch to provide information for rendering.
212
176
 
213
177
    History objects are expected to be short lived -- when serving a request
215
179
    around it, serve the request, throw the History object away, unlock the
216
180
    branch and throw it away.
217
181
 
218
 
    :ivar _file_change_cache: An object that caches information about the
219
 
        files that changed between two revisions.
220
 
    :ivar _rev_info: A list of information about revisions.  This is by far
221
 
        the most cryptic data structure in loggerhead.  At the top level, it
222
 
        is a list of 3-tuples [(merge-info, where-merged, parents)].
223
 
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
224
 
        like a merged sorted list, but the revno is stringified.
225
 
        `where-merged` is a tuple of revisions that have this revision as a
226
 
        non-lefthand parent.  Finally, `parents` is just the usual list of
227
 
        parents of this revision.
228
 
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
229
 
        the information about it in _rev_info.
230
 
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
231
 
        ids.
 
182
    :ivar _file_change_cache: xx
232
183
    """
233
184
 
234
 
    def _load_whole_history_data(self, caches, cache_key):
235
 
        """Set the attributes relating to the whole history of the branch.
236
 
 
237
 
        :param caches: a list of caches with interfaces like
238
 
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
239
 
        :param cache_key: the key to use with the caches.
240
 
        """
241
 
        self._rev_indices = None
242
 
        self._rev_info = None
243
 
 
244
 
        missed_caches = []
245
 
        def update_missed_caches():
246
 
            for cache in missed_caches:
247
 
                cache.set(cache_key, self.last_revid, self._rev_info)
248
 
 
249
 
        # Theoretically, it's possible for two threads to race in creating
250
 
        # the Lock() object for their branch, so we put a lock around
251
 
        # creating the per-branch Lock().
252
 
        revision_graph_check_lock.acquire()
253
 
        try:
254
 
            if cache_key not in revision_graph_locks:
255
 
                revision_graph_locks[cache_key] = threading.Lock()
256
 
        finally:
257
 
            revision_graph_check_lock.release()
258
 
 
259
 
        revision_graph_locks[cache_key].acquire()
260
 
        try:
261
 
            for cache in caches:
262
 
                data = cache.get(cache_key, self.last_revid)
263
 
                if data is not None:
264
 
                    self._rev_info = data
265
 
                    update_missed_caches()
266
 
                    break
267
 
                else:
268
 
                    missed_caches.append(cache)
269
 
            else:
270
 
                whole_history_data = compute_whole_history_data(self._branch)
271
 
                self._rev_info, self._rev_indices = whole_history_data
272
 
                update_missed_caches()
273
 
        finally:
274
 
            revision_graph_locks[cache_key].release()
275
 
 
276
 
        if self._rev_indices is not None:
277
 
            self._revno_revid = {}
278
 
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
279
 
                self._revno_revid[revno_str] = revid
280
 
        else:
281
 
            self._revno_revid = {}
282
 
            self._rev_indices = {}
283
 
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
284
 
                self._rev_indices[revid] = seq
285
 
                self._revno_revid[revno_str] = revid
286
 
 
287
 
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
288
 
                 revinfo_disk_cache=None, cache_key=None):
 
185
    def __init__(self, branch, whole_history_data_cache):
289
186
        assert branch.is_locked(), (
290
187
            "Can only construct a History object with a read-locked branch.")
291
 
        if file_cache is not None:
292
 
            self._file_change_cache = file_cache
293
 
            file_cache.history = self
294
 
        else:
295
 
            self._file_change_cache = None
 
188
        self._file_change_cache = None
296
189
        self._branch = branch
297
 
        self._branch_tags = None
298
 
        self._inventory_cache = {}
299
 
        self._branch_nick = self._branch.get_config().get_nickname()
300
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
 
190
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
301
191
 
302
192
        self.last_revid = branch.last_revision()
303
193
 
304
 
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
305
 
        if revinfo_disk_cache:
306
 
            caches.append(revinfo_disk_cache)
307
 
        self._load_whole_history_data(caches, cache_key)
 
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
308
205
 
309
206
    @property
310
207
    def has_revisions(self):
314
211
        return self._branch.get_config()
315
212
 
316
213
    def get_revno(self, revid):
317
 
        if revid not in self._rev_indices:
 
214
        if revid not in self._revision_info:
318
215
            # ghost parent?
319
216
            return 'unknown'
320
 
        seq = self._rev_indices[revid]
321
 
        revno = self._rev_info[seq][0][3]
322
 
        return revno
 
217
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
218
        return revno_str
323
219
 
324
220
    def get_revids_from(self, revid_list, start_revid):
325
221
        """
327
223
        revid in revid_list.
328
224
        """
329
225
        if revid_list is None:
330
 
            revid_list = [r[0][1] for r in self._rev_info]
 
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 True:
 
237
        while 1:
344
238
            if bzrlib.revision.is_null(revid):
345
239
                return
346
240
            if introduced_revisions(revid) & revid_set:
347
241
                yield revid
348
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
242
            parents = self._revision_graph[revid]
349
243
            if len(parents) == 0:
350
244
                return
351
245
            revid = parents[0]
352
246
 
353
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
354
250
        # FIXME: would be awesome if we could get, for a folder, the list of
355
 
        # revisions where items within that folder changed.i
356
 
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
357
 
        get_parent_map = self._branch.repository.texts.get_parent_map
358
 
        # We chunk the requests as this works better with GraphIndex.
359
 
        # See _filter_revisions_touching_file_id in bzrlib/log.py
360
 
        # for more information.
361
 
        revids = []
362
 
        chunk_size = 1000
363
 
        for start in xrange(0, len(possible_keys), chunk_size):
364
 
            next_keys = possible_keys[start:start + chunk_size]
365
 
            revids += [k[1] for k in get_parent_map(next_keys)]
366
 
        del possible_keys, next_keys
367
 
        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()]
368
255
 
369
256
    def get_revision_history_since(self, revid_list, date):
370
257
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
371
258
        # so start at midnight on 02-sep.
372
259
        date = date + datetime.timedelta(days=1)
373
 
        # our revid list is sorted in REVERSE date order,
374
 
        # so go thru some hoops here...
 
260
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
375
261
        revid_list.reverse()
376
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
377
 
                                                   self._branch.repository),
378
 
                              date)
 
262
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
379
263
        if index == 0:
380
264
            return []
381
265
        revid_list.reverse()
387
271
        given a "quick-search" query, try a few obvious possible meanings:
388
272
 
389
273
            - revision id or # ("128.1.3")
390
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
391
 
iso style "yyyy-mm-dd")
 
274
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
392
275
            - comment text as a fallback
393
276
 
394
277
        and return a revid list that matches.
397
280
        # all the relevant changes (time-consuming) only to return a list of
398
281
        # revids which will be used to fetch a set of changes again.
399
282
 
400
 
        # if they entered a revid, just jump straight there;
401
 
        # ignore the passed-in revid_list
 
283
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
402
284
        revid = self.fix_revid(query)
403
285
        if revid is not None:
404
286
            if isinstance(revid, unicode):
405
287
                revid = revid.encode('utf-8')
406
 
            changes = self.get_changes([revid])
 
288
            changes = self.get_changes([ revid ])
407
289
            if (changes is not None) and (len(changes) > 0):
408
 
                return [revid]
 
290
                return [ revid ]
409
291
 
410
292
        date = None
411
293
        m = self.us_date_re.match(query)
412
294
        if m is not None:
413
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
414
 
                                     int(m.group(1)),
415
 
                                     int(m.group(2)))
 
295
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
416
296
        else:
417
297
            m = self.earth_date_re.match(query)
418
298
            if m is not None:
419
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
420
 
                                         int(m.group(2)),
421
 
                                         int(m.group(1)))
 
299
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
422
300
            else:
423
301
                m = self.iso_date_re.match(query)
424
302
                if m is not None:
425
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
426
 
                                             int(m.group(2)),
427
 
                                             int(m.group(3)))
 
303
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
428
304
        if date is not None:
429
305
            if revid_list is None:
430
 
                # if no limit to the query was given,
431
 
                # search only the direct-parent path.
 
306
                # if no limit to the query was given, search only the direct-parent path.
432
307
                revid_list = list(self.get_revids_from(None, self.last_revid))
433
308
            return self.get_revision_history_since(revid_list, date)
434
309
 
446
321
            return revid
447
322
        if revid == 'head:':
448
323
            return self.last_revid
449
 
        try:
450
 
            if self.revno_re.match(revid):
451
 
                revid = self._revno_revid[revid]
452
 
        except KeyError:
453
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
324
        if self.revno_re.match(revid):
 
325
            revid = self._revno_revid[revid]
454
326
        return revid
455
327
 
456
328
    def get_file_view(self, revid, file_id):
527
399
            return None, None, []
528
400
 
529
401
    def get_inventory(self, revid):
530
 
        if revid not in self._inventory_cache:
531
 
            self._inventory_cache[revid] = (
532
 
                self._branch.repository.get_inventory(revid))
533
 
        return self._inventory_cache[revid]
 
402
        return self._branch.repository.get_revision_inventory(revid)
534
403
 
535
404
    def get_path(self, revid, file_id):
536
405
        if (file_id is None) or (file_id == ''):
537
406
            return ''
538
 
        path = self.get_inventory(revid).id2path(file_id)
 
407
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
539
408
        if (len(path) > 0) and not path.startswith('/'):
540
409
            path = '/' + path
541
410
        return path
543
412
    def get_file_id(self, revid, path):
544
413
        if (len(path) > 0) and not path.startswith('/'):
545
414
            path = '/' + path
546
 
        return self.get_inventory(revid).path2id(path)
 
415
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
547
416
 
548
417
    def get_merge_point_list(self, revid):
549
418
        """
554
423
 
555
424
        merge_point = []
556
425
        while True:
557
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
426
            children = self._where_merged.get(revid, [])
558
427
            nexts = []
559
428
            for child in children:
560
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
429
                child_parents = self._revision_graph[child]
561
430
                if child_parents[0] == revid:
562
431
                    nexts.append(child)
563
432
                else:
583
452
            revnol = revno.split(".")
584
453
            revnos = ".".join(revnol[:-2])
585
454
            revnolast = int(revnol[-1])
586
 
            if revnos in d:
 
455
            if d.has_key(revnos):
587
456
                m = d[revnos][0]
588
457
                if revnolast < m:
589
 
                    d[revnos] = (revnolast, revid)
 
458
                    d[revnos] = ( revnolast, revid )
590
459
            else:
591
 
                d[revnos] = (revnolast, revid)
592
 
 
593
 
        return [revid for (_, revid) in d.itervalues()]
594
 
 
595
 
    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):
596
465
        """
597
 
        given a 'change', fill in the branch nicks on all parents and merge
598
 
        points.
 
466
        given a list of changes from L{get_changes}, fill in the branch nicks
 
467
        on all parents and merge points.
599
468
        """
600
469
        fetch_set = set()
601
 
        for p in change.parents:
602
 
            fetch_set.add(p.revid)
603
 
        for p in change.merge_points:
604
 
            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)
605
475
        p_changes = self.get_changes(list(fetch_set))
606
476
        p_change_dict = dict([(c.revid, c) for c in p_changes])
607
 
        for p in change.parents:
608
 
            if p.revid in p_change_dict:
609
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
610
 
            else:
611
 
                p.branch_nick = '(missing)'
612
 
        for p in change.merge_points:
613
 
            if p.revid in p_change_dict:
614
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
615
 
            else:
616
 
                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)'
617
489
 
618
490
    def get_changes(self, revid_list):
619
491
        """Return a list of changes objects for the given revids.
627
499
        # some data needs to be recalculated each time, because it may
628
500
        # change as new revisions are added.
629
501
        for change in changes:
630
 
            merge_revids = self.simplify_merge_point_list(
631
 
                               self.get_merge_point_list(change.revid))
632
 
            change.merge_points = [
633
 
                util.Container(revid=r,
634
 
                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]
635
504
            if len(change.parents) > 0:
636
 
                change.parents = [util.Container(revid=r,
 
505
                change.parents = [util.Container(revid=r, 
637
506
                    revno=self.get_revno(r)) for r in change.parents]
638
507
            change.revno = self.get_revno(change.revid)
639
508
 
648
517
        # FIXME: deprecated method in getting a null revision
649
518
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
650
519
                            revid_list)
651
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
652
 
                         revid_list)
 
520
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
653
521
        # We need to return the answer in the same order as the input,
654
522
        # less any ghosts.
655
523
        present_revids = [revid for revid in revid_list
658
526
 
659
527
        return [self._change_from_revision(rev) for rev in rev_list]
660
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
 
661
558
    def _change_from_revision(self, revision):
662
559
        """
663
560
        Given a bzrlib Revision, return a processed "change" for use in
664
561
        templates.
665
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
 
666
567
        message, short_message = clean_message(revision.message)
667
568
 
668
 
        if self._branch_tags is None:
669
 
            self._branch_tags = self._branch.tags.get_reverse_tag_dict()
670
 
 
671
 
        revtags = None
672
 
        if revision.revision_id in self._branch_tags:
673
 
          revtags = ', '.join(self._branch_tags[revision.revision_id])
674
 
 
675
569
        entry = {
676
570
            'revid': revision.revision_id,
677
 
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
678
 
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
679
 
            'authors': revision.get_apparent_authors(),
 
571
            'date': commit_time,
 
572
            'author': revision.get_apparent_author(),
680
573
            'branch_nick': revision.properties.get('branch-nick', None),
681
574
            'short_comment': short_message,
682
575
            'comment': revision.message,
683
576
            'comment_clean': [util.html_clean(s) for s in message],
684
577
            'parents': revision.parent_ids,
685
 
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
686
 
            'tags': revtags,
687
578
        }
688
 
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
689
 
            foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
690
 
        elif ":" in revision.revision_id:
691
 
            try:
692
 
                foreign_revid, mapping = \
693
 
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
694
 
                        revision.revision_id)
695
 
            except bzrlib.errors.InvalidRevisionId:
696
 
                foreign_revid = None
697
 
                mapping = None
698
 
        else:
699
 
            foreign_revid = None
700
 
        if foreign_revid is not None:
701
 
            entry["foreign_vcs"] = mapping.vcs.abbreviation
702
 
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
703
579
        return util.Container(entry)
704
580
 
705
 
    def get_file_changes_uncached(self, entry):
706
 
        if entry.parents:
707
 
            old_revid = entry.parents[0].revid
708
 
        else:
709
 
            old_revid = bzrlib.revision.NULL_REVISION
710
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
711
 
 
712
 
    def get_file_changes(self, entry):
 
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):
713
587
        if self._file_change_cache is None:
714
 
            return self.get_file_changes_uncached(entry)
 
588
            return self.get_file_changes_uncached(entries)
715
589
        else:
716
 
            return self._file_change_cache.get_file_changes(entry)
717
 
 
718
 
    def add_changes(self, entry):
719
 
        changes = self.get_file_changes(entry)
720
 
        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
721
615
 
722
616
    def get_file(self, file_id, revid):
723
 
        """Returns (path, filename, file contents)"""
 
617
        "returns (path, filename, data)"
724
618
        inv = self.get_inventory(revid)
725
619
        inv_entry = inv[file_id]
726
620
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
729
623
            path = '/' + path
730
624
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
731
625
 
732
 
    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):
733
709
        """
734
710
        Return a nested data structure containing the changes in a delta::
735
711
 
739
715
            modified: list(
740
716
                filename: str,
741
717
                file_id: str,
742
 
            ),
743
 
            text_changes: list((filename, file_id)),
744
 
        """
745
 
        repo = self._branch.repository
746
 
        if (bzrlib.revision.is_null(old_revid) or
747
 
            bzrlib.revision.is_null(new_revid)):
748
 
            old_tree, new_tree = map(
749
 
                repo.revision_tree, [old_revid, new_revid])
750
 
        else:
751
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
752
 
 
753
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
754
 
 
755
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
756
 
 
757
 
        return util.Container(
758
 
            added=sorted(reporter.added, key=lambda x: x.filename),
759
 
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
760
 
            removed=sorted(reporter.removed, key=lambda x: x.filename),
761
 
            modified=sorted(reporter.modified, key=lambda x: x.filename),
762
 
            text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))
 
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,))