~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: John Arbash Meinel
  • Date: 2011-02-10 05:49:24 UTC
  • mto: This revision was merged to the branch mainline in revision 424.
  • Revision ID: john@arbash-meinel.com-20110210054924-tdxcko62y6ouizq9
Address bugs #716201, and #716217.

Don't expand a template if we get a HEAD request.

Show diffs side-by-side

added added

removed removed

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