~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
 
 
156
 
class RevInfoMemoryCache(object):
157
 
    """A store that validates values against the revids they were stored with.
158
 
 
159
 
    We use a unique key for each branch.
160
 
 
161
 
    The reason for not just using the revid as the key is so that when a new
162
 
    value is provided for a branch, we replace the old value used for the
163
 
    branch.
164
 
 
165
 
    There is another implementation of the same interface in
166
 
    loggerhead.changecache.RevInfoDiskCache.
167
 
    """
168
 
 
169
 
    def __init__(self, cache):
170
 
        self._cache = cache
171
 
 
172
 
    def get(self, key, revid):
173
 
        """Return the data associated with `key`, subject to a revid check.
174
 
 
175
 
        If a value was stored under `key`, with the same revid, return it.
176
 
        Otherwise return None.
177
 
        """
178
 
        cached = self._cache.get(key)
179
 
        if cached is None:
180
 
            return None
181
 
        stored_revid, data = cached
182
 
        if revid == stored_revid:
183
 
            return data
184
 
        else:
185
 
            return None
186
 
 
187
 
    def set(self, key, revid, data):
188
 
        """Store `data` under `key`, to be checked against `revid` on get().
189
 
        """
190
 
        self._cache[key] = (revid, data)
191
 
 
192
 
# Used to store locks that prevent multiple threads from building a 
193
 
# revision graph for the same branch at the same time, because that can
194
 
# cause severe performance issues that are so bad that the system seems
195
 
# to hang.
196
 
revision_graph_locks = {}
197
 
revision_graph_check_lock = threading.Lock()
198
 
 
199
 
class History(object):
 
173
 
 
174
class History (object):
200
175
    """Decorate a branch to provide information for rendering.
201
176
 
202
177
    History objects are expected to be short lived -- when serving a request
204
179
    around it, serve the request, throw the History object away, unlock the
205
180
    branch and throw it away.
206
181
 
207
 
    :ivar _file_change_cache: An object that caches information about the
208
 
        files that changed between two revisions.
209
 
    :ivar _rev_info: A list of information about revisions.  This is by far
210
 
        the most cryptic data structure in loggerhead.  At the top level, it
211
 
        is a list of 3-tuples [(merge-info, where-merged, parents)].
212
 
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
213
 
        like a merged sorted list, but the revno is stringified.
214
 
        `where-merged` is a tuple of revisions that have this revision as a
215
 
        non-lefthand parent.  Finally, `parents` is just the usual list of
216
 
        parents of this revision.
217
 
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
218
 
        the information about it in _rev_info.
219
 
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
220
 
        ids.
 
182
    :ivar _file_change_cache: xx
221
183
    """
222
184
 
223
 
    def _load_whole_history_data(self, caches, cache_key):
224
 
        """Set the attributes relating to the whole history of the branch.
225
 
 
226
 
        :param caches: a list of caches with interfaces like
227
 
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
228
 
        :param cache_key: the key to use with the caches.
229
 
        """
230
 
        self._rev_indices = None
231
 
        self._rev_info = None
232
 
 
233
 
        missed_caches = []
234
 
        def update_missed_caches():
235
 
            for cache in missed_caches:
236
 
                cache.set(cache_key, self.last_revid, self._rev_info)
237
 
 
238
 
        # Theoretically, it's possible for two threads to race in creating
239
 
        # the Lock() object for their branch, so we put a lock around
240
 
        # creating the per-branch Lock().
241
 
        revision_graph_check_lock.acquire()
242
 
        try:
243
 
            if cache_key not in revision_graph_locks:
244
 
                revision_graph_locks[cache_key] = threading.Lock()
245
 
        finally:
246
 
            revision_graph_check_lock.release()
247
 
 
248
 
        revision_graph_locks[cache_key].acquire()
249
 
        try:
250
 
            for cache in caches:
251
 
                data = cache.get(cache_key, self.last_revid)
252
 
                if data is not None:
253
 
                    self._rev_info = data
254
 
                    update_missed_caches()
255
 
                    break
256
 
                else:
257
 
                    missed_caches.append(cache)
258
 
            else:
259
 
                whole_history_data = compute_whole_history_data(self._branch)
260
 
                self._rev_info, self._rev_indices = whole_history_data
261
 
                update_missed_caches()
262
 
        finally:
263
 
            revision_graph_locks[cache_key].release()
264
 
 
265
 
        if self._rev_indices is not None:
266
 
            self._revno_revid = {}
267
 
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
268
 
                self._revno_revid[revno_str] = revid
269
 
        else:
270
 
            self._revno_revid = {}
271
 
            self._rev_indices = {}
272
 
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
273
 
                self._rev_indices[revid] = seq
274
 
                self._revno_revid[revno_str] = revid
275
 
 
276
 
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
277
 
                 revinfo_disk_cache=None, cache_key=None):
 
185
    def __init__(self, branch, whole_history_data_cache):
278
186
        assert branch.is_locked(), (
279
187
            "Can only construct a History object with a read-locked branch.")
280
 
        if file_cache is not None:
281
 
            self._file_change_cache = file_cache
282
 
            file_cache.history = self
283
 
        else:
284
 
            self._file_change_cache = None
 
188
        self._file_change_cache = None
285
189
        self._branch = branch
286
 
        self._inventory_cache = {}
287
 
        self._branch_nick = self._branch.get_config().get_nickname()
288
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
 
190
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
289
191
 
290
192
        self.last_revid = branch.last_revision()
291
193
 
292
 
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
293
 
        if revinfo_disk_cache:
294
 
            caches.append(revinfo_disk_cache)
295
 
        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
296
205
 
297
206
    @property
298
207
    def has_revisions(self):
302
211
        return self._branch.get_config()
303
212
 
304
213
    def get_revno(self, revid):
305
 
        if revid not in self._rev_indices:
 
214
        if revid not in self._revision_info:
306
215
            # ghost parent?
307
216
            return 'unknown'
308
 
        seq = self._rev_indices[revid]
309
 
        revno = self._rev_info[seq][0][3]
310
 
        return revno
 
217
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
218
        return revno_str
311
219
 
312
220
    def get_revids_from(self, revid_list, start_revid):
313
221
        """
315
223
        revid in revid_list.
316
224
        """
317
225
        if revid_list is None:
318
 
            revid_list = [r[0][1] for r in self._rev_info]
 
226
            revid_list = self._full_history
319
227
        revid_set = set(revid_list)
320
228
        revid = start_revid
321
 
 
322
229
        def introduced_revisions(revid):
323
230
            r = set([revid])
324
 
            seq = self._rev_indices[revid]
325
 
            md = self._rev_info[seq][0][2]
 
231
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
326
232
            i = seq + 1
327
 
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
328
 
                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])
329
235
                i += 1
330
236
            return r
331
 
        while True:
 
237
        while 1:
332
238
            if bzrlib.revision.is_null(revid):
333
239
                return
334
240
            if introduced_revisions(revid) & revid_set:
335
241
                yield revid
336
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
242
            parents = self._revision_graph[revid]
337
243
            if len(parents) == 0:
338
244
                return
339
245
            revid = parents[0]
340
246
 
341
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
342
250
        # FIXME: would be awesome if we could get, for a folder, the list of
343
 
        # revisions where items within that folder changed.i
344
 
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
345
 
        get_parent_map = self._branch.repository.texts.get_parent_map
346
 
        # We chunk the requests as this works better with GraphIndex.
347
 
        # See _filter_revisions_touching_file_id in bzrlib/log.py
348
 
        # for more information.
349
 
        revids = []
350
 
        chunk_size = 1000
351
 
        for start in xrange(0, len(possible_keys), chunk_size):
352
 
            next_keys = possible_keys[start:start + chunk_size]
353
 
            revids += [k[1] for k in get_parent_map(next_keys)]
354
 
        del possible_keys, next_keys
355
 
        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()]
356
255
 
357
256
    def get_revision_history_since(self, revid_list, date):
358
257
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
359
258
        # so start at midnight on 02-sep.
360
259
        date = date + datetime.timedelta(days=1)
361
 
        # our revid list is sorted in REVERSE date order,
362
 
        # so go thru some hoops here...
 
260
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
363
261
        revid_list.reverse()
364
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
365
 
                                                   self._branch.repository),
366
 
                              date)
 
262
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
367
263
        if index == 0:
368
264
            return []
369
265
        revid_list.reverse()
375
271
        given a "quick-search" query, try a few obvious possible meanings:
376
272
 
377
273
            - revision id or # ("128.1.3")
378
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
379
 
iso style "yyyy-mm-dd")
 
274
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
380
275
            - comment text as a fallback
381
276
 
382
277
        and return a revid list that matches.
385
280
        # all the relevant changes (time-consuming) only to return a list of
386
281
        # revids which will be used to fetch a set of changes again.
387
282
 
388
 
        # if they entered a revid, just jump straight there;
389
 
        # ignore the passed-in revid_list
 
283
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
390
284
        revid = self.fix_revid(query)
391
285
        if revid is not None:
392
286
            if isinstance(revid, unicode):
393
287
                revid = revid.encode('utf-8')
394
 
            changes = self.get_changes([revid])
 
288
            changes = self.get_changes([ revid ])
395
289
            if (changes is not None) and (len(changes) > 0):
396
 
                return [revid]
 
290
                return [ revid ]
397
291
 
398
292
        date = None
399
293
        m = self.us_date_re.match(query)
400
294
        if m is not None:
401
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
402
 
                                     int(m.group(1)),
403
 
                                     int(m.group(2)))
 
295
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
404
296
        else:
405
297
            m = self.earth_date_re.match(query)
406
298
            if m is not None:
407
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
408
 
                                         int(m.group(2)),
409
 
                                         int(m.group(1)))
 
299
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
410
300
            else:
411
301
                m = self.iso_date_re.match(query)
412
302
                if m is not None:
413
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
414
 
                                             int(m.group(2)),
415
 
                                             int(m.group(3)))
 
303
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
416
304
        if date is not None:
417
305
            if revid_list is None:
418
 
                # if no limit to the query was given,
419
 
                # search only the direct-parent path.
 
306
                # if no limit to the query was given, search only the direct-parent path.
420
307
                revid_list = list(self.get_revids_from(None, self.last_revid))
421
308
            return self.get_revision_history_since(revid_list, date)
422
309
 
434
321
            return revid
435
322
        if revid == 'head:':
436
323
            return self.last_revid
437
 
        try:
438
 
            if self.revno_re.match(revid):
439
 
                revid = self._revno_revid[revid]
440
 
        except KeyError:
441
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
324
        if self.revno_re.match(revid):
 
325
            revid = self._revno_revid[revid]
442
326
        return revid
443
327
 
444
328
    def get_file_view(self, revid, file_id):
515
399
            return None, None, []
516
400
 
517
401
    def get_inventory(self, revid):
518
 
        if revid not in self._inventory_cache:
519
 
            self._inventory_cache[revid] = (
520
 
                self._branch.repository.get_inventory(revid))
521
 
        return self._inventory_cache[revid]
 
402
        return self._branch.repository.get_revision_inventory(revid)
522
403
 
523
404
    def get_path(self, revid, file_id):
524
405
        if (file_id is None) or (file_id == ''):
525
406
            return ''
526
 
        path = self.get_inventory(revid).id2path(file_id)
 
407
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
527
408
        if (len(path) > 0) and not path.startswith('/'):
528
409
            path = '/' + path
529
410
        return path
531
412
    def get_file_id(self, revid, path):
532
413
        if (len(path) > 0) and not path.startswith('/'):
533
414
            path = '/' + path
534
 
        return self.get_inventory(revid).path2id(path)
 
415
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
535
416
 
536
417
    def get_merge_point_list(self, revid):
537
418
        """
542
423
 
543
424
        merge_point = []
544
425
        while True:
545
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
426
            children = self._where_merged.get(revid, [])
546
427
            nexts = []
547
428
            for child in children:
548
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
429
                child_parents = self._revision_graph[child]
549
430
                if child_parents[0] == revid:
550
431
                    nexts.append(child)
551
432
                else:
571
452
            revnol = revno.split(".")
572
453
            revnos = ".".join(revnol[:-2])
573
454
            revnolast = int(revnol[-1])
574
 
            if revnos in d:
 
455
            if d.has_key(revnos):
575
456
                m = d[revnos][0]
576
457
                if revnolast < m:
577
 
                    d[revnos] = (revnolast, revid)
 
458
                    d[revnos] = ( revnolast, revid )
578
459
            else:
579
 
                d[revnos] = (revnolast, revid)
580
 
 
581
 
        return [revid for (_, revid) in d.itervalues()]
582
 
 
583
 
    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):
584
465
        """
585
 
        given a 'change', fill in the branch nicks on all parents and merge
586
 
        points.
 
466
        given a list of changes from L{get_changes}, fill in the branch nicks
 
467
        on all parents and merge points.
587
468
        """
588
469
        fetch_set = set()
589
 
        for p in change.parents:
590
 
            fetch_set.add(p.revid)
591
 
        for p in change.merge_points:
592
 
            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)
593
475
        p_changes = self.get_changes(list(fetch_set))
594
476
        p_change_dict = dict([(c.revid, c) for c in p_changes])
595
 
        for p in change.parents:
596
 
            if p.revid in p_change_dict:
597
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
598
 
            else:
599
 
                p.branch_nick = '(missing)'
600
 
        for p in change.merge_points:
601
 
            if p.revid in p_change_dict:
602
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
603
 
            else:
604
 
                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)'
605
489
 
606
490
    def get_changes(self, revid_list):
607
491
        """Return a list of changes objects for the given revids.
615
499
        # some data needs to be recalculated each time, because it may
616
500
        # change as new revisions are added.
617
501
        for change in changes:
618
 
            merge_revids = self.simplify_merge_point_list(
619
 
                               self.get_merge_point_list(change.revid))
620
 
            change.merge_points = [
621
 
                util.Container(revid=r,
622
 
                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]
623
504
            if len(change.parents) > 0:
624
 
                change.parents = [util.Container(revid=r,
 
505
                change.parents = [util.Container(revid=r, 
625
506
                    revno=self.get_revno(r)) for r in change.parents]
626
507
            change.revno = self.get_revno(change.revid)
627
508
 
636
517
        # FIXME: deprecated method in getting a null revision
637
518
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
638
519
                            revid_list)
639
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
640
 
                         revid_list)
 
520
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
641
521
        # We need to return the answer in the same order as the input,
642
522
        # less any ghosts.
643
523
        present_revids = [revid for revid in revid_list
646
526
 
647
527
        return [self._change_from_revision(rev) for rev in rev_list]
648
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
 
649
558
    def _change_from_revision(self, revision):
650
559
        """
651
560
        Given a bzrlib Revision, return a processed "change" for use in
652
561
        templates.
653
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
 
654
567
        message, short_message = clean_message(revision.message)
655
568
 
656
 
        tags = self._branch.tags.get_reverse_tag_dict()
657
 
 
658
 
        revtags = None
659
 
        if tags.has_key(revision.revision_id):
660
 
          revtags = ', '.join(tags[revision.revision_id])
661
 
 
662
569
        entry = {
663
570
            'revid': revision.revision_id,
664
 
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
665
 
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
666
 
            'authors': revision.get_apparent_authors(),
 
571
            'date': commit_time,
 
572
            'author': revision.get_apparent_author(),
667
573
            'branch_nick': revision.properties.get('branch-nick', None),
668
574
            'short_comment': short_message,
669
575
            'comment': revision.message,
670
576
            'comment_clean': [util.html_clean(s) for s in message],
671
577
            'parents': revision.parent_ids,
672
 
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
673
 
            'tags': revtags,
674
578
        }
675
 
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
676
 
            foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
677
 
        elif ":" in revision.revision_id:
678
 
            try:
679
 
                foreign_revid, mapping = \
680
 
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
681
 
                        revision.revision_id)
682
 
            except bzrlib.errors.InvalidRevisionId:
683
 
                foreign_revid = None
684
 
                mapping = None
685
 
        else:
686
 
            foreign_revid = None
687
 
        if foreign_revid is not None:
688
 
            entry["foreign_vcs"] = mapping.vcs.abbreviation
689
 
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
690
579
        return util.Container(entry)
691
580
 
692
 
    def get_file_changes_uncached(self, entry):
693
 
        if entry.parents:
694
 
            old_revid = entry.parents[0].revid
695
 
        else:
696
 
            old_revid = bzrlib.revision.NULL_REVISION
697
 
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
698
 
 
699
 
    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):
700
587
        if self._file_change_cache is None:
701
 
            return self.get_file_changes_uncached(entry)
 
588
            return self.get_file_changes_uncached(entries)
702
589
        else:
703
 
            return self._file_change_cache.get_file_changes(entry)
704
 
 
705
 
    def add_changes(self, entry):
706
 
        changes = self.get_file_changes(entry)
707
 
        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
708
615
 
709
616
    def get_file(self, file_id, revid):
710
 
        """Returns (path, filename, file contents)"""
 
617
        "returns (path, filename, data)"
711
618
        inv = self.get_inventory(revid)
712
619
        inv_entry = inv[file_id]
713
620
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
716
623
            path = '/' + path
717
624
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
718
625
 
719
 
    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):
720
709
        """
721
710
        Return a nested data structure containing the changes in a delta::
722
711
 
726
715
            modified: list(
727
716
                filename: str,
728
717
                file_id: str,
729
 
            ),
730
 
            text_changes: list((filename, file_id)),
731
 
        """
732
 
        repo = self._branch.repository
733
 
        if (bzrlib.revision.is_null(old_revid) or
734
 
            bzrlib.revision.is_null(new_revid)):
735
 
            old_tree, new_tree = map(
736
 
                repo.revision_tree, [old_revid, new_revid])
737
 
        else:
738
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
739
 
 
740
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
741
 
 
742
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
743
 
 
744
 
        return util.Container(
745
 
            added=sorted(reporter.added, key=lambda x: x.filename),
746
 
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
747
 
            removed=sorted(reporter.removed, key=lambda x: x.filename),
748
 
            modified=sorted(reporter.modified, key=lambda x: x.filename),
749
 
            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,))