177
191
class History (object):
179
193
def __init__(self):
180
194
self._change_cache = None
195
self._file_change_cache = None
181
196
self._index = None
182
197
self._lock = threading.RLock()
185
200
def from_branch(cls, branch, name=None):
188
203
self._branch = branch
189
self._history = branch.revision_history()
190
self._last_revid = self._history[-1]
204
self._last_revid = self._branch.last_revision()
191
205
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
194
208
name = self._branch.nick
195
209
self._name = name
196
210
self.log = logging.getLogger('loggerhead.%s' % (name,))
198
212
self._full_history = []
199
213
self._revision_info = {}
200
214
self._revno_revid = {}
201
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
215
if bzrlib.revision.is_null(self._last_revid):
216
self._merge_sort = []
218
self._merge_sort = bzrlib.tsort.merge_sort(
219
self._revision_graph, self._last_revid, generate_revno=True)
203
220
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
204
221
self._full_history.append(revid)
205
222
revno_str = '.'.join(str(n) for n in revno)
206
223
self._revno_revid[revno_str] = revid
207
224
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
211
225
# cache merge info
212
226
self._where_merged = {}
213
227
for revid in self._revision_graph.keys():
214
if not revid in self._full_history:
228
if self._revision_info[revid][2] == 0:
216
230
for parent in self._revision_graph[revid]:
217
231
self._where_merged.setdefault(parent, set()).add(revid)
219
233
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
223
237
def from_folder(cls, path, name=None):
224
238
b = bzrlib.branch.Branch.open(path)
225
return cls.from_branch(b, name)
241
return cls.from_branch(b, name)
227
245
@with_branch_lock
228
246
def out_of_date(self):
229
if self._branch.revision_history()[-1] != self._last_revid:
247
# the branch may have been upgraded on disk, in which case we're stale.
248
newly_opened = bzrlib.branch.Branch.open(self._branch.base)
249
if self._branch.__class__ is not \
250
newly_opened.__class__:
252
if self._branch.repository.__class__ is not \
253
newly_opened.repository.__class__:
255
return self._branch.last_revision() != self._last_revid
233
257
def use_cache(self, cache):
234
258
self._change_cache = cache
260
def use_file_cache(self, cache):
261
self._file_change_cache = cache
236
263
def use_search_index(self, index):
237
264
self._index = index
267
def has_revisions(self):
268
return not bzrlib.revision.is_null(self.last_revid)
239
270
@with_branch_lock
240
271
def detach(self):
241
272
# called when a new history object needs to be created, because the
421
433
# if a "revid" is actually a dotted revno, convert it to a revid
422
434
if revid is None:
437
return self._last_revid
424
438
if self.revno_re.match(revid):
425
439
revid = self._revno_revid[revid]
428
442
@with_branch_lock
429
443
def get_file_view(self, revid, file_id):
431
Given an optional revid and optional path, return a (revlist, revid)
432
for navigation through the current scope: from the revid (or the
433
latest revision) back to the original revision.
445
Given a revid and optional path, return a (revlist, revid) for
446
navigation through the current scope: from the revid (or the latest
447
revision) back to the original revision.
435
449
If file_id is None, the entire revision history is the list scope.
436
If revid is None, the latest revision is used.
438
451
if revid is None:
439
452
revid = self._last_revid
440
453
if file_id is not None:
441
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
442
inv = self._branch.repository.get_revision_inventory(revid)
454
# since revid is 'start_revid', possibly should start the path
455
# tracing from revid... FIXME
443
456
revlist = list(self.get_short_revision_history_by_fileid(file_id))
444
457
revlist = list(self.get_revids_from(revlist, revid))
446
459
revlist = list(self.get_revids_from(None, revid))
449
return revlist, revid
451
462
@with_branch_lock
452
463
def get_view(self, revid, start_revid, file_id, query=None):
454
465
use the URL parameters (revid, start_revid, file_id, and query) to
455
466
determine the revision list we're viewing (start_revid, file_id, query)
456
467
and where we are in it (revid).
458
if a query is given, we're viewing query results.
459
if a file_id is given, we're viewing revisions for a specific file.
460
if a start_revid is given, we're viewing the branch from a
461
specific revision up the tree.
462
(these may be combined to view revisions for a specific file, from
463
a specific revision, with a specific search query.)
465
returns a new (revid, start_revid, revid_list, scan_list) where:
469
- if a query is given, we're viewing query results.
470
- if a file_id is given, we're viewing revisions for a specific
472
- if a start_revid is given, we're viewing the branch from a
473
specific revision up the tree.
475
these may be combined to view revisions for a specific file, from
476
a specific revision, with a specific search query.
478
returns a new (revid, start_revid, revid_list) where:
467
480
- revid: current position within the view
468
481
- start_revid: starting revision of this view
469
482
- revid_list: list of revision ids for this view
471
484
file_id and query are never changed so aren't returned, but they may
472
485
contain vital context for future url navigation.
487
if start_revid is None:
488
start_revid = self._last_revid
474
490
if query is None:
475
revid_list, start_revid = self.get_file_view(start_revid, file_id)
491
revid_list = self.get_file_view(start_revid, file_id)
476
492
if revid is None:
477
493
revid = start_revid
478
494
if revid not in revid_list:
479
495
# if the given revid is not in the revlist, use a revlist that
480
496
# starts at the given revid.
481
revid_list, start_revid = self.get_file_view(revid, file_id)
497
revid_list= self.get_file_view(revid, file_id)
482
499
return revid, start_revid, revid_list
484
501
# potentially limit the search
485
if (start_revid is not None) or (file_id is not None):
486
revid_list, start_revid = self.get_file_view(start_revid, file_id)
502
if file_id is not None:
503
revid_list = self.get_file_view(start_revid, file_id)
488
505
revid_list = None
576
594
p_changes = self.get_changes(list(fetch_set))
577
595
p_change_dict = dict([(c.revid, c) for c in p_changes])
578
596
for change in changes:
597
# arch-converted branches may not have merged branch info :(
579
598
for p in change.parents:
580
p.branch_nick = p_change_dict[p.revid].branch_nick
599
if p.revid in p_change_dict:
600
p.branch_nick = p_change_dict[p.revid].branch_nick
602
p.branch_nick = '(missing)'
581
603
for p in change.merge_points:
582
p.branch_nick = p_change_dict[p.revid].branch_nick
604
if p.revid in p_change_dict:
605
p.branch_nick = p_change_dict[p.revid].branch_nick
607
p.branch_nick = '(missing)'
584
609
@with_branch_lock
585
def get_changes(self, revid_list, get_diffs=False):
610
def get_changes(self, revid_list):
611
"""Return a list of changes objects for the given revids.
613
Revisions not present and NULL_REVISION will be ignored.
586
615
if self._change_cache is None:
587
changes = self.get_changes_uncached(revid_list, get_diffs)
616
changes = self.get_changes_uncached(revid_list)
589
changes = self._change_cache.get_changes(revid_list, get_diffs)
618
changes = self._change_cache.get_changes(revid_list)
619
if len(changes) == 0:
593
622
# some data needs to be recalculated each time, because it may
594
623
# change as new revisions are added.
595
for i in xrange(len(revid_list)):
596
revid = revid_list[i]
598
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
624
for change in changes:
625
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
599
626
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
627
if len(change.parents) > 0:
628
if isinstance(change.parents[0], util.Container):
629
# old cache stored a potentially-bogus revno
630
change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
632
change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
633
change.revno = self.get_revno(change.revid)
636
for change in changes:
637
change.parity = parity
603
# alright, let's profile this sucka.
604
def _get_changes_profiled(self, revid_list, get_diffs=False):
642
# alright, let's profile this sucka. (FIXME remove this eventually...)
643
def _get_changes_profiled(self, revid_list):
605
644
from loggerhead.lsprof import profile
607
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
646
ret, stats = profile(self.get_changes_uncached, revid_list)
610
649
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
611
650
self.log.info('lsprof complete!')
654
@with_bzrlib_read_lock
655
def get_changes_uncached(self, revid_list):
656
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
658
repo = self._branch.repository
659
parent_map = repo.get_graph().get_parent_map(revid_list)
660
# We need to return the answer in the same order as the input,
662
present_revids = [revid for revid in revid_list
663
if revid in parent_map]
664
rev_list = repo.get_revisions(present_revids)
666
return [self._change_from_revision(rev) for rev in rev_list]
614
668
def _get_deltas_for_revisions_with_trees(self, revisions):
615
"""Produce a generator of revision deltas.
669
"""Produce a list of revision deltas.
617
671
Note that the input is a sequence of REVISIONS, not revision_ids.
618
672
Trees will be held in memory until the generator exits.
619
673
Each delta is relative to the revision's lefthand predecessor.
674
(This is copied from bzrlib.)
621
676
required_trees = set()
622
677
for revision in revisions:
623
required_trees.add(revision.revision_id)
624
required_trees.update(revision.parent_ids[:1])
625
trees = dict((t.get_revision_id(), t) for
678
required_trees.add(revision.revid)
679
required_trees.update([p.revid for p in revision.parents[:1]])
680
trees = dict((t.get_revision_id(), t) for
626
681
t in self._branch.repository.revision_trees(required_trees))
628
683
self._branch.repository.lock_read()
630
685
for revision in revisions:
631
if not revision.parent_ids:
632
old_tree = self._branch.repository.revision_tree(None)
686
if not revision.parents:
687
old_tree = self._branch.repository.revision_tree(
688
bzrlib.revision.NULL_REVISION)
634
old_tree = trees[revision.parent_ids[0]]
635
tree = trees[revision.revision_id]
636
ret.append((tree, old_tree, tree.changes_from(old_tree)))
690
old_tree = trees[revision.parents[0].revid]
691
tree = trees[revision.revid]
692
ret.append(tree.changes_from(old_tree))
639
695
self._branch.repository.unlock()
641
def entry_from_revision(self, revision):
697
def _change_from_revision(self, revision):
699
Given a bzrlib Revision, return a processed "change" for use in
642
702
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
644
704
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
646
if len(parents) == 0:
649
left_parent = revision.parent_ids[0]
651
706
message, short_message = clean_message(revision.message)
654
709
'revid': revision.revision_id,
655
'revno': self.get_revno(revision.revision_id),
656
710
'date': commit_time,
657
711
'author': revision.committer,
658
712
'branch_nick': revision.properties.get('branch-nick', None),
659
713
'short_comment': short_message,
660
714
'comment': revision.message,
661
715
'comment_clean': [util.html_clean(s) for s in message],
716
'parents': revision.parent_ids,
664
718
return util.Container(entry)
667
@with_bzrlib_read_lock
668
def get_changes_uncached(self, revid_list, get_diffs=False):
670
rev_list = self._branch.repository.get_revisions(revid_list)
671
except (KeyError, bzrlib.errors.NoSuchRevision):
674
delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
675
combined_list = zip(rev_list, delta_list)
678
for rev, (new_tree, old_tree, delta) in combined_list:
679
entry = self.entry_from_revision(rev)
680
entry.changes = self.parse_delta(delta, get_diffs, old_tree, new_tree)
681
entries.append(entry)
685
@with_bzrlib_read_lock
686
def _get_diff(self, revid1, revid2):
687
rev_tree1 = self._branch.repository.revision_tree(revid1)
688
rev_tree2 = self._branch.repository.revision_tree(revid2)
720
def get_file_changes_uncached(self, entries):
721
delta_list = self._get_deltas_for_revisions_with_trees(entries)
723
return [self.parse_delta(delta) for delta in delta_list]
726
def get_file_changes(self, entries):
727
if self._file_change_cache is None:
728
return self.get_file_changes_uncached(entries)
730
return self._file_change_cache.get_file_changes(entries)
732
def add_changes(self, entries):
733
changes_list = self.get_file_changes(entries)
735
for entry, changes in zip(entries, changes_list):
736
entry.changes = changes
739
def get_change_with_diff(self, revid, compare_revid=None):
740
change = self.get_changes([revid])[0]
742
if compare_revid is None:
744
compare_revid = change.parents[0].revid
746
compare_revid = 'null:'
748
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
749
rev_tree2 = self._branch.repository.revision_tree(revid)
689
750
delta = rev_tree2.changes_from(rev_tree1)
690
return rev_tree1, rev_tree2, delta
692
def get_diff(self, revid1, revid2):
693
rev_tree1, rev_tree2, delta = self._get_diff(revid1, revid2)
694
entry = self.get_changes([ revid2 ], False)[0]
695
entry.changes = self.parse_delta(delta, True, rev_tree1, rev_tree2)
752
change.changes = self.parse_delta(delta)
753
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
698
757
@with_branch_lock
699
758
def get_file(self, file_id, revid):
700
759
"returns (path, filename, data)"
730
if C{get_diffs} is false, the C{chunks} will be omitted.
788
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
790
process.append((old_path, new_path, fid, kind))
791
for path, fid, kind, text_modified, meta_modified in delta.modified:
792
process.append((path, path, fid, kind))
794
for old_path, new_path, fid, kind in process:
795
old_lines = old_tree.get_file_lines(fid)
796
new_lines = new_tree.get_file_lines(fid)
798
if old_lines != new_lines:
800
bzrlib.diff.internal_diff(old_path, old_lines,
801
new_path, new_lines, buffer)
802
except bzrlib.errors.BinaryFile:
805
diff = buffer.getvalue()
808
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
812
def _process_diff(self, diff):
813
# doesn't really need to be a method; could be static.
816
for line in diff.splitlines():
819
if line.startswith('+++ ') or line.startswith('--- '):
821
if line.startswith('@@ '):
823
if chunk is not None:
825
chunk = util.Container()
827
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
828
old_lineno = lines[0]
829
new_lineno = lines[1]
830
elif line.startswith(' '):
831
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
832
type='context', line=util.fixed_width(line[1:])))
835
elif line.startswith('+'):
836
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
837
type='insert', line=util.fixed_width(line[1:])))
839
elif line.startswith('-'):
840
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
841
type='delete', line=util.fixed_width(line[1:])))
844
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
845
type='unknown', line=util.fixed_width(repr(line))))
846
if chunk is not None:
850
def parse_delta(self, delta):
852
Return a nested data structure containing the changes in a delta::
854
added: list((filename, file_id)),
855
renamed: list((old_filename, new_filename, file_id)),
856
deleted: list((filename, file_id)),
737
def rich_filename(path, kind):
738
if kind == 'directory':
740
if kind == 'symlink':
744
def process_diff(diff):
747
for line in diff.splitlines():
750
if line.startswith('+++ ') or line.startswith('--- '):
752
if line.startswith('@@ '):
754
if chunk is not None:
756
chunk = util.Container()
758
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
759
old_lineno = lines[0]
760
new_lineno = lines[1]
761
elif line.startswith(' '):
762
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
763
type='context', line=util.html_clean(line[1:])))
766
elif line.startswith('+'):
767
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
768
type='insert', line=util.html_clean(line[1:])))
770
elif line.startswith('-'):
771
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
772
type='delete', line=util.html_clean(line[1:])))
775
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
776
type='unknown', line=util.html_clean(repr(line))))
777
if chunk is not None:
781
def handle_modify(old_path, new_path, fid, kind):
783
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
785
old_lines = old_tree.get_file_lines(fid)
786
new_lines = new_tree.get_file_lines(fid)
788
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
789
diff = buffer.getvalue()
790
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
792
867
for path, fid, kind in delta.added:
793
868
added.append((rich_filename(path, kind), fid))
795
870
for path, fid, kind, text_modified, meta_modified in delta.modified:
796
handle_modify(path, path, fid, kind)
798
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
799
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
871
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
873
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
874
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
800
875
if meta_modified or text_modified:
801
handle_modify(oldpath, newpath, fid, kind)
876
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
803
878
for path, fid, kind in delta.removed:
804
879
removed.append((rich_filename(path, kind), fid))
806
881
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
811
886
for change in changes:
812
887
for m in change.changes.modified:
813
888
m.sbs_chunks = _make_side_by_side(m.chunks)
815
890
@with_branch_lock
816
def get_filelist(self, inv, path, sort_type=None):
891
def get_filelist(self, inv, file_id, sort_type=None):
818
893
return the list of all files (and their attributes) within a given
821
while path.endswith('/'):
823
if path.startswith('/'):
826
entries = inv.entries()
829
for filepath, entry in entries:
830
fetch_set.add(entry.revision)
831
change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
897
dir_ie = inv[file_id]
898
path = inv.id2path(file_id)
834
for filepath, entry in entries:
835
if posixpath.dirname(filepath) != path:
837
filename = posixpath.basename(filepath)
838
rich_filename = filename
903
for filename, entry in dir_ie.children.iteritems():
904
revid_set.add(entry.revision)
907
for change in self.get_changes(list(revid_set)):
908
change_dict[change.revid] = change
910
for filename, entry in dir_ie.children.iteritems():
839
911
pathname = filename
840
912
if entry.kind == 'directory':
844
915
revid = entry.revision
845
change = change_dict[revid]
847
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
848
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
917
file = util.Container(
918
filename=filename, executable=entry.executable, kind=entry.kind,
919
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
920
revid=revid, change=change_dict[revid])
849
921
file_list.append(file)
851
if sort_type == 'filename':
923
if sort_type == 'filename' or sort_type is None:
852
924
file_list.sort(key=lambda x: x.filename)
853
925
elif sort_type == 'size':
854
926
file_list.sort(key=lambda x: x.size)
855
927
elif sort_type == 'date':
856
928
file_list.sort(key=lambda x: x.change.date)
859
931
for file in file_list:
860
932
file.parity = parity