174
161
class History (object):
175
"""Decorate a branch to provide information for rendering.
177
History objects are expected to be short lived -- when serving a request
178
for a particular branch, open it, read-lock it, wrap a History object
179
around it, serve the request, throw the History object away, unlock the
180
branch and throw it away.
182
:ivar _file_change_cache: xx
185
def __init__(self, branch, whole_history_data_cache):
186
assert branch.is_locked(), (
187
"Can only construct a History object with a read-locked branch.")
188
self._file_change_cache = None
164
self._change_cache = None
166
self._lock = threading.RLock()
169
def from_branch(cls, branch, name=None):
189
172
self._branch = branch
190
self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
192
self.last_revid = branch.last_revision()
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
199
(self._revision_graph, self._full_history, self._revision_info,
200
self._revno_revid, self._merge_sort, self._where_merged
201
) = whole_history_data
203
def use_file_cache(self, cache):
204
self._file_change_cache = cache
207
def has_revisions(self):
208
return not bzrlib.revision.is_null(self.last_revid)
173
self._history = branch.revision_history()
174
self._last_revid = self._history[-1]
175
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
178
name = self._branch.nick
180
self.log = logging.getLogger('loggerhead.%s' % (name,))
182
self._full_history = []
183
self._revision_info = {}
184
self._revno_revid = {}
185
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
187
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
188
self._full_history.append(revid)
189
revno_str = '.'.join(str(n) for n in revno)
190
self._revno_revid[revno_str] = revid
191
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
196
self._where_merged = {}
197
for revid in self._revision_graph.keys():
198
if not revid in self._full_history:
200
for parent in self._revision_graph[revid]:
201
self._where_merged.setdefault(parent, set()).add(revid)
203
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
207
def from_folder(cls, path, name=None):
208
b = bzrlib.branch.Branch.open(path)
209
return cls.from_branch(b, name)
212
def out_of_date(self):
213
if self._branch.revision_history()[-1] != self._last_revid:
217
def use_cache(self, cache):
218
self._change_cache = cache
220
def use_search_index(self, index):
225
# called when a new history object needs to be created, because the
226
# branch history has changed. we need to immediately close and stop
227
# using our caches, because a new history object will be created to
228
# replace us, using the same cache files.
229
# (may also be called during server shutdown.)
230
if self._change_cache is not None:
231
self._change_cache.close()
232
self._change_cache = None
233
if self._index is not None:
237
def flush_cache(self):
238
if self._change_cache is None:
240
self._change_cache.flush()
242
def check_rebuild(self):
243
if self._change_cache is not None:
244
self._change_cache.check_rebuild()
245
if self._index is not None:
246
self._index.check_rebuild()
248
last_revid = property(lambda self: self._last_revid, None, None)
250
count = property(lambda self: self._count, None, None)
210
253
def get_config(self):
211
254
return self._branch.get_config()
257
def get_revision(self, revid):
258
return self._branch.repository.get_revision(revid)
213
260
def get_revno(self, revid):
214
261
if revid not in self._revision_info:
217
264
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
220
def get_revids_from(self, revid_list, start_revid):
222
Yield the mainline (wrt start_revid) revisions that merged each
225
if revid_list is None:
226
revid_list = self._full_history
227
revid_set = set(revid_list)
229
def introduced_revisions(revid):
231
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
233
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
234
r.add(self._merge_sort[i][1])
238
if bzrlib.revision.is_null(revid):
240
if introduced_revisions(revid) & revid_set:
267
def get_sequence(self, revid):
268
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
271
def get_revision_history(self):
272
return self._full_history
274
def get_revid_sequence(self, revid_list, revid):
276
given a list of revision ids, return the sequence # of this revid in
285
def get_revids_from(self, revid_list, revid):
287
given a list of revision ids, yield revisions in graph order,
288
starting from revid. the list can be None if you just want to travel
289
across all revisions.
292
if (revid_list is None) or (revid in revid_list):
294
if not self._revision_graph.has_key(revid):
242
296
parents = self._revision_graph[revid]
243
297
if len(parents) == 0:
245
299
revid = parents[0]
247
302
def get_short_revision_history_by_fileid(self, file_id):
248
303
# wow. is this really the only way we can get this list? by
249
304
# man-handling the weave store directly? :-0
320
405
# if a "revid" is actually a dotted revno, convert it to a revid
321
406
if revid is None:
324
return self.last_revid
325
408
if self.revno_re.match(revid):
326
409
revid = self._revno_revid[revid]
329
413
def get_file_view(self, revid, file_id):
331
Given a revid and optional path, return a (revlist, revid) for
332
navigation through the current scope: from the revid (or the latest
333
revision) back to the original revision.
415
Given an optional revid and optional path, return a (revlist, revid)
416
for navigation through the current scope: from the revid (or the
417
latest revision) back to the original revision.
335
419
If file_id is None, the entire revision history is the list scope.
420
If revid is None, the latest revision is used.
337
422
if revid is None:
338
revid = self.last_revid
423
revid = self._last_revid
339
424
if file_id is not None:
340
# since revid is 'start_revid', possibly should start the path
341
# tracing from revid... FIXME
425
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
426
inv = self._branch.repository.get_revision_inventory(revid)
342
427
revlist = list(self.get_short_revision_history_by_fileid(file_id))
343
428
revlist = list(self.get_revids_from(revlist, revid))
345
430
revlist = list(self.get_revids_from(None, revid))
433
return revlist, revid
348
436
def get_view(self, revid, start_revid, file_id, query=None):
350
438
use the URL parameters (revid, start_revid, file_id, and query) to
351
439
determine the revision list we're viewing (start_revid, file_id, query)
352
440
and where we are in it (revid).
354
- if a query is given, we're viewing query results.
355
- if a file_id is given, we're viewing revisions for a specific
357
- if a start_revid is given, we're viewing the branch from a
358
specific revision up the tree.
360
these may be combined to view revisions for a specific file, from
361
a specific revision, with a specific search query.
363
returns a new (revid, start_revid, revid_list) where:
442
if a query is given, we're viewing query results.
443
if a file_id is given, we're viewing revisions for a specific file.
444
if a start_revid is given, we're viewing the branch from a
445
specific revision up the tree.
446
(these may be combined to view revisions for a specific file, from
447
a specific revision, with a specific search query.)
449
returns a new (revid, start_revid, revid_list, scan_list) where:
365
451
- revid: current position within the view
366
452
- start_revid: starting revision of this view
367
453
- revid_list: list of revision ids for this view
369
455
file_id and query are never changed so aren't returned, but they may
370
456
contain vital context for future url navigation.
372
if start_revid is None:
373
start_revid = self.last_revid
375
458
if query is None:
376
revid_list = self.get_file_view(start_revid, file_id)
459
revid_list, start_revid = self.get_file_view(start_revid, file_id)
377
460
if revid is None:
378
461
revid = start_revid
379
462
if revid not in revid_list:
380
463
# if the given revid is not in the revlist, use a revlist that
381
464
# starts at the given revid.
382
revid_list = self.get_file_view(revid, file_id)
465
revid_list, start_revid = self.get_file_view(revid, file_id)
384
466
return revid, start_revid, revid_list
386
468
# potentially limit the search
387
if file_id is not None:
388
revid_list = self.get_file_view(start_revid, file_id)
469
if (start_revid is not None) or (file_id is not None):
470
revid_list, start_revid = self.get_file_view(start_revid, file_id)
390
472
revid_list = None
392
474
revid_list = self.get_search_revid_list(query, revid_list)
393
if revid_list and len(revid_list) > 0:
475
if len(revid_list) > 0:
394
476
if revid not in revid_list:
395
477
revid = revid_list[0]
396
478
return revid, start_revid, revid_list
398
481
return None, None, []
400
484
def get_inventory(self, revid):
401
485
return self._branch.repository.get_revision_inventory(revid)
403
488
def get_path(self, revid, file_id):
404
489
if (file_id is None) or (file_id == ''):
474
560
p_changes = self.get_changes(list(fetch_set))
475
561
p_change_dict = dict([(c.revid, c) for c in p_changes])
476
562
for change in changes:
477
# arch-converted branches may not have merged branch info :(
478
563
for p in change.parents:
479
if p.revid in p_change_dict:
480
p.branch_nick = p_change_dict[p.revid].branch_nick
482
p.branch_nick = '(missing)'
564
p.branch_nick = p_change_dict[p.revid].branch_nick
483
565
for p in change.merge_points:
484
if p.revid in p_change_dict:
485
p.branch_nick = p_change_dict[p.revid].branch_nick
487
p.branch_nick = '(missing)'
489
def get_changes(self, revid_list):
490
"""Return a list of changes objects for the given revids.
492
Revisions not present and NULL_REVISION will be ignored.
494
changes = self.get_changes_uncached(revid_list)
495
if len(changes) == 0:
566
p.branch_nick = p_change_dict[p.revid].branch_nick
569
def get_changes(self, revid_list, get_diffs=False):
570
if self._change_cache is None:
571
changes = self.get_changes_uncached(revid_list, get_diffs)
573
changes = self._change_cache.get_changes(revid_list, get_diffs)
498
577
# some data needs to be recalculated each time, because it may
499
578
# change as new revisions are added.
500
for change in changes:
501
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
579
for i in xrange(len(revid_list)):
580
revid = revid_list[i]
582
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
502
583
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
503
if len(change.parents) > 0:
504
change.parents = [util.Container(revid=r,
505
revno=self.get_revno(r)) for r in change.parents]
506
change.revno = self.get_revno(change.revid)
509
for change in changes:
510
change.parity = parity
515
def get_changes_uncached(self, revid_list):
516
# FIXME: deprecated method in getting a null revision
517
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
519
parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
520
# We need to return the answer in the same order as the input,
522
present_revids = [revid for revid in revid_list
523
if revid in parent_map]
524
rev_list = self._branch.repository.get_revisions(present_revids)
526
return [self._change_from_revision(rev) for rev in rev_list]
528
def _get_deltas_for_revisions_with_trees(self, revisions):
529
"""Produce a list of revision deltas.
531
Note that the input is a sequence of REVISIONS, not revision_ids.
532
Trees will be held in memory until the generator exits.
533
Each delta is relative to the revision's lefthand predecessor.
534
(This is copied from bzrlib.)
536
required_trees = set()
537
for revision in revisions:
538
required_trees.add(revision.revid)
539
required_trees.update([p.revid for p in revision.parents[:1]])
540
trees = dict((t.get_revision_id(), t) for
541
t in self._branch.repository.revision_trees(required_trees))
543
self._branch.repository.lock_read()
587
# alright, let's profile this sucka.
588
def _get_changes_profiled(self, revid_list, get_diffs=False):
589
from loggerhead.lsprof import profile
591
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
594
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
598
@with_bzrlib_read_lock
599
def get_changes_uncached(self, revid_list, get_diffs=False):
545
for revision in revisions:
546
if not revision.parents:
547
old_tree = self._branch.repository.revision_tree(
548
bzrlib.revision.NULL_REVISION)
550
old_tree = trees[revision.parents[0].revid]
551
tree = trees[revision.revid]
552
ret.append(tree.changes_from(old_tree))
555
self._branch.repository.unlock()
557
def _change_from_revision(self, revision):
559
Given a bzrlib Revision, return a processed "change" for use in
562
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
564
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
566
message, short_message = clean_message(revision.message)
569
'revid': revision.revision_id,
571
'author': revision.get_apparent_author(),
572
'branch_nick': revision.properties.get('branch-nick', None),
573
'short_comment': short_message,
574
'comment': revision.message,
575
'comment_clean': [util.html_clean(s) for s in message],
576
'parents': revision.parent_ids,
578
return util.Container(entry)
580
def get_file_changes_uncached(self, entries):
581
delta_list = self._get_deltas_for_revisions_with_trees(entries)
583
return [self.parse_delta(delta) for delta in delta_list]
585
def get_file_changes(self, entries):
586
if self._file_change_cache is None:
587
return self.get_file_changes_uncached(entries)
589
return self._file_change_cache.get_file_changes(entries)
591
def add_changes(self, entries):
592
changes_list = self.get_file_changes(entries)
594
for entry, changes in zip(entries, changes_list):
595
entry.changes = changes
597
def get_change_with_diff(self, revid, compare_revid=None):
598
change = self.get_changes([revid])[0]
600
if compare_revid is None:
602
compare_revid = change.parents[0].revid
601
rev_list = self._branch.repository.get_revisions(revid_list)
602
except (KeyError, bzrlib.errors.NoSuchRevision):
605
delta_list = self._branch.repository.get_deltas_for_revisions(rev_list)
606
combined_list = zip(rev_list, delta_list)
610
# lookup the trees for each revision, so we can calculate diffs
613
lookup_set.add(rev.revision_id)
614
if len(rev.parent_ids) > 0:
615
lookup_set.add(rev.parent_ids[0])
616
tree_map = dict((t.get_revision_id(), t) for t in self._branch.repository.revision_trees(lookup_set))
617
# also the root tree, in case we hit the origin:
618
tree_map[None] = self._branch.repository.revision_tree(None)
621
for rev, delta in combined_list:
622
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
624
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
626
if len(parents) == 0:
604
compare_revid = 'null:'
606
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
607
rev_tree2 = self._branch.repository.revision_tree(revid)
608
delta = rev_tree2.changes_from(rev_tree1)
610
change.changes = self.parse_delta(delta)
611
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
629
left_parent = rev.parent_ids[0]
631
message = rev.message.splitlines()
632
if len(message) == 1:
633
# robey-style 1-line long message
634
message = textwrap.wrap(message[0])
636
# make short form of commit message
637
short_message = message[0]
638
if len(short_message) > 60:
639
short_message = short_message[:60] + '...'
641
old_tree, new_tree = None, None
643
new_tree = tree_map[rev.revision_id]
644
old_tree = tree_map[left_parent]
647
'revid': rev.revision_id,
648
'revno': self.get_revno(rev.revision_id),
650
'author': rev.committer,
651
'branch_nick': rev.properties.get('branch-nick', None),
652
'short_comment': short_message,
653
'comment': rev.message,
654
'comment_clean': [util.html_clean(s) for s in message],
656
'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
658
entries.append(util.Container(entry))
615
663
def get_file(self, file_id, revid):
616
"returns (path, filename, data)"
617
inv = self.get_inventory(revid)
618
inv_entry = inv[file_id]
664
"returns (filename, data)"
665
inv_entry = self.get_inventory(revid)[file_id]
619
666
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
620
path = inv.id2path(file_id)
621
if not path.startswith('/'):
623
return path, inv_entry.name, rev_tree.get_file_text(file_id)
625
def _parse_diffs(self, old_tree, new_tree, delta):
667
return inv_entry.name, rev_tree.get_file_text(file_id)
670
def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
627
Return a list of processed diffs, in the format::
672
Return a nested data structure containing the changes in a delta::
674
added: list((filename, file_id)),
675
renamed: list((old_filename, new_filename, file_id)),
676
deleted: list((filename, file_id)),
645
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
647
process.append((old_path, new_path, fid, kind))
648
for path, fid, kind, text_modified, meta_modified in delta.modified:
649
process.append((path, path, fid, kind))
651
for old_path, new_path, fid, kind in process:
652
old_lines = old_tree.get_file_lines(fid)
653
new_lines = new_tree.get_file_lines(fid)
655
if old_lines != new_lines:
657
bzrlib.diff.internal_diff(old_path, old_lines,
658
new_path, new_lines, buffer)
659
except bzrlib.errors.BinaryFile:
662
diff = buffer.getvalue()
665
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
669
def _process_diff(self, diff):
670
# doesn't really need to be a method; could be static.
673
for line in diff.splitlines():
676
if line.startswith('+++ ') or line.startswith('--- '):
678
if line.startswith('@@ '):
680
if chunk is not None:
682
chunk = util.Container()
684
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
685
old_lineno = lines[0]
686
new_lineno = lines[1]
687
elif line.startswith(' '):
688
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
689
type='context', line=util.fixed_width(line[1:])))
692
elif line.startswith('+'):
693
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
694
type='insert', line=util.fixed_width(line[1:])))
696
elif line.startswith('-'):
697
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
698
type='delete', line=util.fixed_width(line[1:])))
701
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
702
type='unknown', line=util.fixed_width(repr(line))))
703
if chunk is not None:
707
def parse_delta(self, delta):
709
Return a nested data structure containing the changes in a delta::
711
added: list((filename, file_id)),
712
renamed: list((old_filename, new_filename, file_id)),
713
deleted: list((filename, file_id)),
690
if C{get_diffs} is false, the C{chunks} will be omitted.
697
def rich_filename(path, kind):
698
if kind == 'directory':
700
if kind == 'symlink':
704
def process_diff(diff):
707
for line in diff.splitlines():
710
if line.startswith('+++ ') or line.startswith('--- '):
712
if line.startswith('@@ '):
714
if chunk is not None:
716
chunk = util.Container()
718
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
719
old_lineno = lines[0]
720
new_lineno = lines[1]
721
elif line.startswith(' '):
722
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
723
type='context', line=util.html_clean(line[1:])))
726
elif line.startswith('+'):
727
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
728
type='insert', line=util.html_clean(line[1:])))
730
elif line.startswith('-'):
731
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
732
type='delete', line=util.html_clean(line[1:])))
735
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
736
type='unknown', line=util.html_clean(repr(line))))
737
if chunk is not None:
741
def handle_modify(old_path, new_path, fid, kind):
743
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
745
old_lines = old_tree.get_file_lines(fid)
746
new_lines = new_tree.get_file_lines(fid)
748
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
749
diff = buffer.getvalue()
750
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
724
752
for path, fid, kind in delta.added:
725
753
added.append((rich_filename(path, kind), fid))
727
755
for path, fid, kind, text_modified, meta_modified in delta.modified:
728
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
730
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
731
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
756
handle_modify(path, path, fid, kind)
758
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
759
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
732
760
if meta_modified or text_modified:
733
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
761
handle_modify(oldpath, newpath, fid, kind)
735
763
for path, fid, kind in delta.removed:
736
764
removed.append((rich_filename(path, kind), fid))
738
766
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
741
def add_side_by_side(changes):
769
def make_side_by_side(changes):
742
770
# FIXME: this is a rotten API.
743
771
for change in changes:
744
772
for m in change.changes.modified:
745
m.sbs_chunks = _make_side_by_side(m.chunks)
747
def get_filelist(self, inv, file_id, sort_type=None):
773
m.chunks = _make_side_by_side(m.chunks)
776
def get_filelist(self, inv, path, sort_type=None):
749
778
return the list of all files (and their attributes) within a given
753
dir_ie = inv[file_id]
754
path = inv.id2path(file_id)
781
while path.endswith('/'):
783
if path.startswith('/'):
786
entries = inv.entries()
789
for filepath, entry in entries:
790
fetch_set.add(entry.revision)
791
change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
759
for filename, entry in dir_ie.children.iteritems():
760
revid_set.add(entry.revision)
763
for change in self.get_changes(list(revid_set)):
764
change_dict[change.revid] = change
766
for filename, entry in dir_ie.children.iteritems():
794
for filepath, entry in entries:
795
if posixpath.dirname(filepath) != path:
797
filename = posixpath.basename(filepath)
798
rich_filename = filename
767
799
pathname = filename
768
800
if entry.kind == 'directory':
771
804
revid = entry.revision
773
file = util.Container(
774
filename=filename, executable=entry.executable, kind=entry.kind,
775
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
776
revid=revid, change=change_dict[revid])
805
change = change_dict[revid]
807
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
808
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
777
809
file_list.append(file)
779
if sort_type == 'filename' or sort_type is None:
780
file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
811
if sort_type == 'filename':
812
file_list.sort(key=lambda x: x.filename)
781
813
elif sort_type == 'size':
782
814
file_list.sort(key=lambda x: x.size)
783
815
elif sort_type == 'date':
784
816
file_list.sort(key=lambda x: x.change.date)
786
# Always sort by kind to get directories first
787
file_list.sort(key=lambda x: x.kind != 'directory')
790
819
for file in file_list:
791
820
file.parity = parity