93
103
out_chunk_list = []
94
104
for chunk in chunk_list:
97
106
delete_list, insert_list = [], []
98
107
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)
104
108
if line.type == 'context':
105
109
if len(delete_list) or len(insert_list):
106
_process_side_by_side_buffers(line_list, delete_list,
110
_process_side_by_side_buffers(line_list, delete_list, insert_list)
108
111
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,
112
line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
113
old_line=line.line, new_line=line.line,
114
old_type=line.type, new_type=line.type))
115
115
elif line.type == 'delete':
116
delete_list.append((line.old_lineno, wrapped_line, line.type))
116
delete_list.append((line.old_lineno, line.line, line.type))
117
117
elif line.type == 'insert':
118
insert_list.append((line.new_lineno, wrapped_line, line.type))
118
insert_list.append((line.new_lineno, line.line, line.type))
119
119
if len(delete_list) or len(insert_list):
120
120
_process_side_by_side_buffers(line_list, delete_list, insert_list)
121
121
out_chunk_list.append(util.Container(diff=line_list))
186
186
class History (object):
187
"""Decorate a branch to provide information for rendering.
189
History objects are expected to be short lived -- when serving a request
190
for a particular branch, open it, read-lock it, wrap a History object
191
around it, serve the request, throw the History object away, unlock the
192
branch and throw it away.
194
:ivar _file_change_cache: xx
197
def __init__(self, branch, whole_history_data_cache):
198
assert branch.is_locked(), (
199
"Can only construct a History object with a read-locked branch.")
189
self._change_cache = None
200
190
self._file_change_cache = None
192
self._lock = threading.RLock()
195
def from_branch(cls, branch, name=None):
201
198
self._branch = branch
202
self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
204
self.last_revid = branch.last_revision()
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
211
(self._revision_graph, self._full_history, self._revision_info,
212
self._revno_revid, self._merge_sort, self._where_merged
213
) = whole_history_data
199
self._last_revid = self._branch.last_revision()
200
if self._last_revid is not None:
201
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
203
self._revision_graph = {}
206
name = self._branch.nick
208
self.log = logging.getLogger('loggerhead.%s' % (name,))
210
self._full_history = []
211
self._revision_info = {}
212
self._revno_revid = {}
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
215
self._full_history.append(revid)
216
revno_str = '.'.join(str(n) for n in revno)
217
self._revno_revid[revno_str] = revid
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
221
self._where_merged = {}
222
for revid in self._revision_graph.keys():
223
if not revid in self._full_history:
225
for parent in self._revision_graph[revid]:
226
self._where_merged.setdefault(parent, set()).add(revid)
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
232
def from_folder(cls, path, name=None):
233
b = bzrlib.branch.Branch.open(path)
234
return cls.from_branch(b, name)
237
def out_of_date(self):
238
# the branch may have been upgraded on disk, in which case we're stale.
239
if self._branch.__class__ is not \
240
bzrlib.branch.Branch.open(self._branch.base).__class__:
242
return self._branch.last_revision() != self._last_revid
244
def use_cache(self, cache):
245
self._change_cache = cache
215
247
def use_file_cache(self, cache):
216
248
self._file_change_cache = cache
219
def has_revisions(self):
220
return not bzrlib.revision.is_null(self.last_revid)
250
def use_search_index(self, index):
255
# called when a new history object needs to be created, because the
256
# branch history has changed. we need to immediately close and stop
257
# using our caches, because a new history object will be created to
258
# replace us, using the same cache files.
259
# (may also be called during server shutdown.)
260
if self._change_cache is not None:
261
self._change_cache.close()
262
self._change_cache = None
263
if self._index is not None:
267
def flush_cache(self):
268
if self._change_cache is None:
270
self._change_cache.flush()
272
def check_rebuild(self):
273
if self._change_cache is not None:
274
self._change_cache.check_rebuild()
275
if self._index is not None:
276
self._index.check_rebuild()
278
last_revid = property(lambda self: self._last_revid, None, None)
222
281
def get_config(self):
223
282
return self._branch.get_config()
229
288
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
232
def get_revids_from(self, revid_list, start_revid):
234
Yield the mainline (wrt start_revid) revisions that merged each
237
if revid_list is None:
238
revid_list = self._full_history
239
revid_set = set(revid_list)
241
def introduced_revisions(revid):
243
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
245
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
246
r.add(self._merge_sort[i][1])
250
if bzrlib.revision.is_null(revid):
252
if introduced_revisions(revid) & revid_set:
291
def get_revision_history(self):
292
return self._full_history
294
def get_revids_from(self, revid_list, revid):
296
given a list of revision ids, yield revisions in graph order,
297
starting from revid. the list can be None if you just want to travel
298
across all revisions.
301
if (revid_list is None) or (revid in revid_list):
303
if not self._revision_graph.has_key(revid):
254
305
parents = self._revision_graph[revid]
255
306
if len(parents) == 0:
257
308
revid = parents[0]
259
311
def get_short_revision_history_by_fileid(self, file_id):
260
312
# wow. is this really the only way we can get this list? by
261
313
# man-handling the weave store directly? :-0
262
314
# FIXME: would be awesome if we could get, for a folder, the list of
263
315
# 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()]
316
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
317
w_revids = w.versions()
318
revids = [r for r in self._full_history if r in w_revids]
268
322
def get_revision_history_since(self, revid_list, date):
269
323
# if a user asks for revisions starting at 01-sep, they mean inclusive,
270
324
# so start at midnight on 02-sep.
279
333
return revid_list[index:]
336
def get_revision_history_matching(self, revid_list, text):
337
self.log.debug('searching %d revisions for %r', len(revid_list), text)
339
# this is going to be painfully slow. :(
342
for revid in revid_list:
343
change = self.get_changes([ revid ])[0]
344
if text in change.comment.lower():
346
self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
349
def get_revision_history_matching_indexed(self, revid_list, text):
350
self.log.debug('searching %d revisions for %r', len(revid_list), text)
352
if self._index is None:
353
return self.get_revision_history_matching(revid_list, text)
354
out = self._index.find(text, revid_list)
355
self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
356
# put them in some coherent order :)
357
out = [r for r in self._full_history if r in out]
281
361
def get_search_revid_list(self, query, revid_list):
283
363
given a "quick-search" query, try a few obvious possible meanings:
356
442
revlist = list(self.get_revids_from(None, revid))
359
446
def get_view(self, revid, start_revid, file_id, query=None):
361
448
use the URL parameters (revid, start_revid, file_id, and query) to
362
449
determine the revision list we're viewing (start_revid, file_id, query)
363
450
and where we are in it (revid).
365
- if a query is given, we're viewing query results.
366
- if a file_id is given, we're viewing revisions for a specific
368
- if a start_revid is given, we're viewing the branch from a
369
specific revision up the tree.
371
these may be combined to view revisions for a specific file, from
372
a specific revision, with a specific search query.
374
returns a new (revid, start_revid, revid_list) where:
452
if a query is given, we're viewing query results.
453
if a file_id is given, we're viewing revisions for a specific file.
454
if a start_revid is given, we're viewing the branch from a
455
specific revision up the tree.
456
(these may be combined to view revisions for a specific file, from
457
a specific revision, with a specific search query.)
459
returns a new (revid, start_revid, revid_list, scan_list) where:
376
461
- revid: current position within the view
377
462
- start_revid: starting revision of this view
399
484
revid_list = self.get_file_view(start_revid, file_id)
401
486
revid_list = None
402
revid_list = search.search_revisions(self._branch, query)
403
if revid_list and len(revid_list) > 0:
488
revid_list = self.get_search_revid_list(query, revid_list)
489
if len(revid_list) > 0:
404
490
if revid not in revid_list:
405
491
revid = revid_list[0]
406
492
return revid, start_revid, revid_list
408
# XXX: This should return a message saying that the search could
409
# not be completed due to either missing the plugin or missing a
411
495
return None, None, []
413
498
def get_inventory(self, revid):
414
499
return self._branch.repository.get_revision_inventory(revid)
416
502
def get_path(self, revid, file_id):
417
503
if (file_id is None) or (file_id == ''):
528
def get_changes_uncached(self, revid_list):
529
# FIXME: deprecated method in getting a null revision
530
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
532
parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
533
# We need to return the answer in the same order as the input,
535
present_revids = [revid for revid in revid_list
536
if revid in parent_map]
537
rev_list = self._branch.repository.get_revisions(present_revids)
539
return [self._change_from_revision(rev) for rev in rev_list]
541
def _get_deltas_for_revisions_with_trees(self, revisions):
542
"""Produce a list of revision deltas.
613
# alright, let's profile this sucka.
614
def _get_changes_profiled(self, revid_list, get_diffs=False):
615
from loggerhead.lsprof import profile
617
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
620
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
621
self.log.info('lsprof complete!')
624
def _get_deltas_for_revisions_with_trees(self, entries):
625
"""Produce a generator of revision deltas.
544
627
Note that the input is a sequence of REVISIONS, not revision_ids.
545
628
Trees will be held in memory until the generator exits.
546
629
Each delta is relative to the revision's lefthand predecessor.
547
(This is copied from bzrlib.)
549
631
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]])
632
for entry in entries:
633
required_trees.add(entry.revid)
634
required_trees.update([p.revid for p in entry.parents[:1]])
553
635
trees = dict((t.get_revision_id(), t) for
554
636
t in self._branch.repository.revision_trees(required_trees))
556
638
self._branch.repository.lock_read()
558
for revision in revisions:
559
if not revision.parents:
640
for entry in entries:
641
if not entry.parents:
560
642
old_tree = self._branch.repository.revision_tree(
561
643
bzrlib.revision.NULL_REVISION)
563
old_tree = trees[revision.parents[0].revid]
564
tree = trees[revision.revid]
645
old_tree = trees[entry.parents[0].revid]
646
tree = trees[entry.revid]
565
647
ret.append(tree.changes_from(old_tree))
568
650
self._branch.repository.unlock()
570
def _change_from_revision(self, revision):
572
Given a bzrlib Revision, return a processed "change" for use in
652
def entry_from_revision(self, revision):
575
653
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
577
655
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
582
660
'revid': revision.revision_id,
583
661
'date': commit_time,
584
'author': revision.get_apparent_author(),
662
'author': revision.committer,
585
663
'branch_nick': revision.properties.get('branch-nick', None),
586
664
'short_comment': short_message,
587
665
'comment': revision.message,
588
666
'comment_clean': [util.html_clean(s) for s in message],
589
'parents': revision.parent_ids,
591
669
return util.Container(entry)
672
def get_changes_uncached(self, revid_list):
673
# Because we may loop and call get_revisions multiple times (to throw
674
# out dud revids), we grab a read lock.
675
self._branch.lock_read()
679
rev_list = self._branch.repository.get_revisions(revid_list)
680
except (KeyError, bzrlib.errors.NoSuchRevision), e:
681
# this sometimes happens with arch-converted branches.
682
# i don't know why. :(
683
self.log.debug('No such revision (skipping): %s', e)
684
revid_list.remove(e.revision)
688
return [self.entry_from_revision(rev) for rev in rev_list]
690
self._branch.unlock()
593
692
def get_file_changes_uncached(self, entries):
594
693
delta_list = self._get_deltas_for_revisions_with_trees(entries)
596
695
return [self.parse_delta(delta) for delta in delta_list]
598
698
def get_file_changes(self, entries):
599
699
if self._file_change_cache is None:
600
700
return self.get_file_changes_uncached(entries)
698
801
old_lineno = lines[0]
699
802
new_lineno = lines[1]
700
803
elif line.startswith(' '):
701
chunk.diff.append(util.Container(old_lineno=old_lineno,
702
new_lineno=new_lineno,
804
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
805
type='context', line=util.fixed_width(line[1:])))
707
808
elif line.startswith('+'):
708
chunk.diff.append(util.Container(old_lineno=None,
709
new_lineno=new_lineno,
710
type='insert', line=line[1:]))
809
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
810
type='insert', line=util.fixed_width(line[1:])))
712
812
elif line.startswith('-'):
713
chunk.diff.append(util.Container(old_lineno=old_lineno,
715
type='delete', line=line[1:]))
813
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
814
type='delete', line=util.fixed_width(line[1:])))
718
chunk.diff.append(util.Container(old_lineno=None,
817
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
818
type='unknown', line=util.fixed_width(repr(line))))
722
819
if chunk is not None:
723
820
chunks.append(chunk)