~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: Jelmer Vernooij
  • Date: 2008-08-06 18:39:15 UTC
  • mto: (197.1.9 pathargs)
  • mto: This revision was merged to the branch mainline in revision 202.
  • Revision ID: jelmer@samba.org-20080806183915-81q0zlnxq57s0egb
Add --pidfile option to stop-loggerhead.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008, 2009 Canonical Ltd.
 
2
# Copyright (C) 2008  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
 
 
 
37
import time
 
38
from StringIO import StringIO
 
39
 
 
40
from loggerhead import search
 
41
from loggerhead import util
 
42
from loggerhead.wholehistory import compute_whole_history_data
 
43
 
 
44
import bzrlib
38
45
import bzrlib.branch
39
 
import bzrlib.delta
 
46
import bzrlib.diff
40
47
import bzrlib.errors
41
 
import bzrlib.foreign
 
48
import bzrlib.progress
42
49
import bzrlib.revision
43
 
 
44
 
from loggerhead import search
45
 
from loggerhead import util
46
 
from loggerhead.wholehistory import compute_whole_history_data
 
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
47
123
 
48
124
 
49
125
def is_branch(folder):
61
137
    module (Robey, the original author of this code, apparently favored this
62
138
    style of message).
63
139
    """
64
 
    message = message.lstrip().splitlines()
 
140
    message = message.splitlines()
65
141
 
66
142
    if len(message) == 1:
67
143
        message = textwrap.wrap(message[0])
88
164
    return path
89
165
 
90
166
 
 
167
 
 
168
# from bzrlib
91
169
class _RevListToTimestamps(object):
92
170
    """This takes a list of revisions, and allows you to bisect by date"""
93
171
 
99
177
 
100
178
    def __getitem__(self, index):
101
179
        """Get the date of the index'd item"""
102
 
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
103
 
                   self.revid_list[index]).timestamp)
 
180
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
104
181
 
105
182
    def __len__(self):
106
183
        return len(self.revid_list)
107
184
 
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):
 
185
 
 
186
class History (object):
211
187
    """Decorate a branch to provide information for rendering.
212
188
 
213
189
    History objects are expected to be short lived -- when serving a request
215
191
    around it, serve the request, throw the History object away, unlock the
216
192
    branch and throw it away.
217
193
 
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.
 
194
    :ivar _file_change_cache: xx
232
195
    """
233
196
 
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):
 
197
    def __init__(self, branch, whole_history_data_cache):
289
198
        assert branch.is_locked(), (
290
199
            "Can only construct a History object with a read-locked branch.")
291
 
        if file_cache is not None:
292
 
            self._file_change_cache = file_cache
293
 
            file_cache.history = self
294
 
        else:
295
 
            self._file_change_cache = None
 
200
        self._file_change_cache = None
296
201
        self._branch = branch
297
 
        self._branch_tags = None
298
 
        self._inventory_cache = {}
299
 
        self._branch_nick = self._branch.get_config().get_nickname()
300
 
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
 
202
        self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
301
203
 
302
204
        self.last_revid = branch.last_revision()
303
205
 
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)
 
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
308
217
 
309
218
    @property
310
219
    def has_revisions(self):
314
223
        return self._branch.get_config()
315
224
 
316
225
    def get_revno(self, revid):
317
 
        if revid not in self._rev_indices:
 
226
        if revid not in self._revision_info:
318
227
            # ghost parent?
319
228
            return 'unknown'
320
 
        seq = self._rev_indices[revid]
321
 
        revno = self._rev_info[seq][0][3]
322
 
        return revno
 
229
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
 
230
        return revno_str
323
231
 
324
232
    def get_revids_from(self, revid_list, start_revid):
325
233
        """
327
235
        revid in revid_list.
328
236
        """
329
237
        if revid_list is None:
330
 
            revid_list = [r[0][1] for r in self._rev_info]
 
238
            revid_list = self._full_history
331
239
        revid_set = set(revid_list)
332
240
        revid = start_revid
333
 
 
334
241
        def introduced_revisions(revid):
335
242
            r = set([revid])
336
 
            seq = self._rev_indices[revid]
337
 
            md = self._rev_info[seq][0][2]
 
243
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
338
244
            i = seq + 1
339
 
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
340
 
                r.add(self._rev_info[i][0][1])
 
245
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
 
246
                r.add(self._merge_sort[i][1])
341
247
                i += 1
342
248
            return r
343
 
        while True:
 
249
        while 1:
344
250
            if bzrlib.revision.is_null(revid):
345
251
                return
346
252
            if introduced_revisions(revid) & revid_set:
347
253
                yield revid
348
 
            parents = self._rev_info[self._rev_indices[revid]][2]
 
254
            parents = self._revision_graph[revid]
349
255
            if len(parents) == 0:
350
256
                return
351
257
            revid = parents[0]
352
258
 
353
259
    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
354
262
        # FIXME: would be awesome if we could get, for a folder, the list of
355
 
        # revisions where items within that folder changed.i
356
 
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
357
 
        get_parent_map = self._branch.repository.texts.get_parent_map
358
 
        # We chunk the requests as this works better with GraphIndex.
359
 
        # See _filter_revisions_touching_file_id in bzrlib/log.py
360
 
        # for more information.
361
 
        revids = []
362
 
        chunk_size = 1000
363
 
        for start in xrange(0, len(possible_keys), chunk_size):
364
 
            next_keys = possible_keys[start:start + chunk_size]
365
 
            revids += [k[1] for k in get_parent_map(next_keys)]
366
 
        del possible_keys, next_keys
367
 
        return revids
 
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()]
368
267
 
369
268
    def get_revision_history_since(self, revid_list, date):
370
269
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
371
270
        # so start at midnight on 02-sep.
372
271
        date = date + datetime.timedelta(days=1)
373
 
        # our revid list is sorted in REVERSE date order,
374
 
        # so go thru some hoops here...
 
272
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
375
273
        revid_list.reverse()
376
 
        index = bisect.bisect(_RevListToTimestamps(revid_list,
377
 
                                                   self._branch.repository),
378
 
                              date)
 
274
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
379
275
        if index == 0:
380
276
            return []
381
277
        revid_list.reverse()
387
283
        given a "quick-search" query, try a few obvious possible meanings:
388
284
 
389
285
            - revision id or # ("128.1.3")
390
 
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
391
 
iso style "yyyy-mm-dd")
 
286
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
392
287
            - comment text as a fallback
393
288
 
394
289
        and return a revid list that matches.
397
292
        # all the relevant changes (time-consuming) only to return a list of
398
293
        # revids which will be used to fetch a set of changes again.
399
294
 
400
 
        # if they entered a revid, just jump straight there;
401
 
        # ignore the passed-in revid_list
 
295
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
402
296
        revid = self.fix_revid(query)
403
297
        if revid is not None:
404
298
            if isinstance(revid, unicode):
405
299
                revid = revid.encode('utf-8')
406
 
            changes = self.get_changes([revid])
 
300
            changes = self.get_changes([ revid ])
407
301
            if (changes is not None) and (len(changes) > 0):
408
 
                return [revid]
 
302
                return [ revid ]
409
303
 
410
304
        date = None
411
305
        m = self.us_date_re.match(query)
412
306
        if m is not None:
413
 
            date = datetime.datetime(util.fix_year(int(m.group(3))),
414
 
                                     int(m.group(1)),
415
 
                                     int(m.group(2)))
 
307
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
416
308
        else:
417
309
            m = self.earth_date_re.match(query)
418
310
            if m is not None:
419
 
                date = datetime.datetime(util.fix_year(int(m.group(3))),
420
 
                                         int(m.group(2)),
421
 
                                         int(m.group(1)))
 
311
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
422
312
            else:
423
313
                m = self.iso_date_re.match(query)
424
314
                if m is not None:
425
 
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
426
 
                                             int(m.group(2)),
427
 
                                             int(m.group(3)))
 
315
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
428
316
        if date is not None:
429
317
            if revid_list is None:
430
 
                # if no limit to the query was given,
431
 
                # search only the direct-parent path.
 
318
                # if no limit to the query was given, search only the direct-parent path.
432
319
                revid_list = list(self.get_revids_from(None, self.last_revid))
433
320
            return self.get_revision_history_since(revid_list, date)
434
321
 
446
333
            return revid
447
334
        if revid == 'head:':
448
335
            return self.last_revid
449
 
        try:
450
 
            if self.revno_re.match(revid):
451
 
                revid = self._revno_revid[revid]
452
 
        except KeyError:
453
 
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
336
        if self.revno_re.match(revid):
 
337
            revid = self._revno_revid[revid]
454
338
        return revid
455
339
 
456
340
    def get_file_view(self, revid, file_id):
527
411
            return None, None, []
528
412
 
529
413
    def get_inventory(self, revid):
530
 
        if revid not in self._inventory_cache:
531
 
            self._inventory_cache[revid] = (
532
 
                self._branch.repository.get_inventory(revid))
533
 
        return self._inventory_cache[revid]
 
414
        return self._branch.repository.get_revision_inventory(revid)
534
415
 
535
416
    def get_path(self, revid, file_id):
536
417
        if (file_id is None) or (file_id == ''):
537
418
            return ''
538
 
        path = self.get_inventory(revid).id2path(file_id)
 
419
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
539
420
        if (len(path) > 0) and not path.startswith('/'):
540
421
            path = '/' + path
541
422
        return path
543
424
    def get_file_id(self, revid, path):
544
425
        if (len(path) > 0) and not path.startswith('/'):
545
426
            path = '/' + path
546
 
        return self.get_inventory(revid).path2id(path)
 
427
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
547
428
 
548
429
    def get_merge_point_list(self, revid):
549
430
        """
554
435
 
555
436
        merge_point = []
556
437
        while True:
557
 
            children = self._rev_info[self._rev_indices[revid]][1]
 
438
            children = self._where_merged.get(revid, [])
558
439
            nexts = []
559
440
            for child in children:
560
 
                child_parents = self._rev_info[self._rev_indices[child]][2]
 
441
                child_parents = self._revision_graph[child]
561
442
                if child_parents[0] == revid:
562
443
                    nexts.append(child)
563
444
                else:
583
464
            revnol = revno.split(".")
584
465
            revnos = ".".join(revnol[:-2])
585
466
            revnolast = int(revnol[-1])
586
 
            if revnos in d:
 
467
            if d.has_key(revnos):
587
468
                m = d[revnos][0]
588
469
                if revnolast < m:
589
 
                    d[revnos] = (revnolast, revid)
 
470
                    d[revnos] = ( revnolast, revid )
590
471
            else:
591
 
                d[revnos] = (revnolast, revid)
592
 
 
593
 
        return [revid for (_, revid) in d.itervalues()]
594
 
 
595
 
    def add_branch_nicks(self, change):
 
472
                d[revnos] = ( revnolast, revid )
 
473
 
 
474
        return [ d[revnos][1] for revnos in d.keys() ]
 
475
 
 
476
    def get_branch_nicks(self, changes):
596
477
        """
597
 
        given a 'change', fill in the branch nicks on all parents and merge
598
 
        points.
 
478
        given a list of changes from L{get_changes}, fill in the branch nicks
 
479
        on all parents and merge points.
599
480
        """
600
481
        fetch_set = set()
601
 
        for p in change.parents:
602
 
            fetch_set.add(p.revid)
603
 
        for p in change.merge_points:
604
 
            fetch_set.add(p.revid)
 
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)
605
487
        p_changes = self.get_changes(list(fetch_set))
606
488
        p_change_dict = dict([(c.revid, c) for c in p_changes])
607
 
        for p in change.parents:
608
 
            if p.revid in p_change_dict:
609
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
610
 
            else:
611
 
                p.branch_nick = '(missing)'
612
 
        for p in change.merge_points:
613
 
            if p.revid in p_change_dict:
614
 
                p.branch_nick = p_change_dict[p.revid].branch_nick
615
 
            else:
616
 
                p.branch_nick = '(missing)'
 
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)'
617
501
 
618
502
    def get_changes(self, revid_list):
619
503
        """Return a list of changes objects for the given revids.
627
511
        # some data needs to be recalculated each time, because it may
628
512
        # change as new revisions are added.
629
513
        for change in changes:
630
 
            merge_revids = self.simplify_merge_point_list(
631
 
                               self.get_merge_point_list(change.revid))
632
 
            change.merge_points = [
633
 
                util.Container(revid=r,
634
 
                revno=self.get_revno(r)) for r in merge_revids]
 
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]
635
516
            if len(change.parents) > 0:
636
 
                change.parents = [util.Container(revid=r,
 
517
                change.parents = [util.Container(revid=r, 
637
518
                    revno=self.get_revno(r)) for r in change.parents]
638
519
            change.revno = self.get_revno(change.revid)
639
520
 
648
529
        # FIXME: deprecated method in getting a null revision
649
530
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
650
531
                            revid_list)
651
 
        parent_map = self._branch.repository.get_graph().get_parent_map(
652
 
                         revid_list)
 
532
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
653
533
        # We need to return the answer in the same order as the input,
654
534
        # less any ghosts.
655
535
        present_revids = [revid for revid in revid_list
658
538
 
659
539
        return [self._change_from_revision(rev) for rev in rev_list]
660
540
 
 
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
 
661
570
    def _change_from_revision(self, revision):
662
571
        """
663
572
        Given a bzrlib Revision, return a processed "change" for use in
664
573
        templates.
665
574
        """
 
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
 
666
579
        message, short_message = clean_message(revision.message)
667
580
 
668
 
        if self._branch_tags is None:
669
 
            self._branch_tags = self._branch.tags.get_reverse_tag_dict()
670
 
 
671
 
        revtags = None
672
 
        if revision.revision_id in self._branch_tags:
673
 
          revtags = ', '.join(self._branch_tags[revision.revision_id])
674
 
 
675
581
        entry = {
676
582
            'revid': revision.revision_id,
677
 
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
678
 
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
679
 
            'authors': revision.get_apparent_authors(),
 
583
            'date': commit_time,
 
584
            'author': revision.get_apparent_author(),
680
585
            'branch_nick': revision.properties.get('branch-nick', None),
681
586
            'short_comment': short_message,
682
587
            'comment': revision.message,
683
588
            'comment_clean': [util.html_clean(s) for s in message],
684
589
            'parents': revision.parent_ids,
685
 
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
686
 
            'tags': revtags,
687
590
        }
688
 
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
689
 
            foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
690
 
        elif ":" in revision.revision_id:
691
 
            try:
692
 
                foreign_revid, mapping = \
693
 
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
694
 
                        revision.revision_id)
695
 
            except bzrlib.errors.InvalidRevisionId:
696
 
                foreign_revid = None
697
 
                mapping = None
698
 
        else:
699
 
            foreign_revid = None
700
 
        if foreign_revid is not None:
701
 
            entry["foreign_vcs"] = mapping.vcs.abbreviation
702
 
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
703
591
        return util.Container(entry)
704
592
 
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):
 
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):
713
599
        if self._file_change_cache is None:
714
 
            return self.get_file_changes_uncached(entry)
 
600
            return self.get_file_changes_uncached(entries)
715
601
        else:
716
 
            return self._file_change_cache.get_file_changes(entry)
717
 
 
718
 
    def add_changes(self, entry):
719
 
        changes = self.get_file_changes(entry)
720
 
        entry.changes = changes
 
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
721
627
 
722
628
    def get_file(self, file_id, revid):
723
 
        """Returns (path, filename, file contents)"""
 
629
        "returns (path, filename, data)"
724
630
        inv = self.get_inventory(revid)
725
631
        inv_entry = inv[file_id]
726
632
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
729
635
            path = '/' + path
730
636
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
731
637
 
732
 
    def file_changes_for_revision_ids(self, old_revid, new_revid):
 
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):
733
727
        """
734
728
        Return a nested data structure containing the changes in a delta::
735
729
 
739
733
            modified: list(
740
734
                filename: str,
741
735
                file_id: str,
742
 
            ),
743
 
            text_changes: list((filename, file_id)),
744
 
        """
745
 
        repo = self._branch.repository
746
 
        if (bzrlib.revision.is_null(old_revid) or
747
 
            bzrlib.revision.is_null(new_revid)):
748
 
            old_tree, new_tree = map(
749
 
                repo.revision_tree, [old_revid, new_revid])
750
 
        else:
751
 
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
752
 
 
753
 
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
754
 
 
755
 
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
756
 
 
757
 
        return util.Container(
758
 
            added=sorted(reporter.added, key=lambda x: x.filename),
759
 
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
760
 
            removed=sorted(reporter.removed, key=lambda x: x.filename),
761
 
            modified=sorted(reporter.modified, key=lambda x: x.filename),
762
 
            text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))
 
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,))