190
180
class History (object):
192
182
def __init__(self):
193
183
self._change_cache = None
194
self._file_change_cache = None
195
184
self._index = None
196
185
self._lock = threading.RLock()
199
188
def from_branch(cls, branch, name=None):
202
191
self._branch = branch
203
self._last_revid = self._branch.last_revision()
192
self._history = branch.revision_history()
193
self._last_revid = self._history[-1]
194
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
206
197
name = self._branch.nick
207
198
self._name = name
208
199
self.log = logging.getLogger('loggerhead.%s' % (name,))
210
graph = branch.repository.get_graph()
211
parent_map = dict(((key, value) for key, value in
212
graph.iter_ancestry([self._last_revid]) if value is not None))
214
self._revision_graph = self._strip_NULL_ghosts(parent_map)
215
201
self._full_history = []
216
202
self._revision_info = {}
217
203
self._revno_revid = {}
218
if bzrlib.revision.is_null(self._last_revid):
219
self._merge_sort = []
221
self._merge_sort = bzrlib.tsort.merge_sort(
222
self._revision_graph, self._last_revid, None,
204
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
225
206
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
226
207
self._full_history.append(revid)
227
208
revno_str = '.'.join(str(n) for n in revno)
228
209
self._revno_revid[revno_str] = revid
229
self._revision_info[revid] = (seq, revid, merge_depth, revno_str,
210
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
231
214
# cache merge info
232
215
self._where_merged = {}
234
216
for revid in self._revision_graph.keys():
235
if self._revision_info[revid][2] == 0:
217
if not revid in self._full_history:
237
219
for parent in self._revision_graph[revid]:
238
220
self._where_merged.setdefault(parent, set()).add(revid)
240
222
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
244
def _strip_NULL_ghosts(revision_graph):
246
Copied over from bzrlib meant as a temporary workaround deprecated
250
# Filter ghosts, and null:
251
if bzrlib.revision.NULL_REVISION in revision_graph:
252
del revision_graph[bzrlib.revision.NULL_REVISION]
253
for key, parents in revision_graph.items():
254
revision_graph[key] = tuple(parent for parent in parents if parent
256
return revision_graph
259
226
def from_folder(cls, path, name=None):
260
227
b = bzrlib.branch.Branch.open(path)
263
return cls.from_branch(b, name)
228
return cls.from_branch(b, name)
267
230
@with_branch_lock
268
231
def out_of_date(self):
269
# the branch may have been upgraded on disk, in which case we're stale.
270
newly_opened = bzrlib.branch.Branch.open(self._branch.base)
271
if self._branch.__class__ is not \
272
newly_opened.__class__:
274
if self._branch.repository.__class__ is not \
275
newly_opened.repository.__class__:
277
return self._branch.last_revision() != self._last_revid
232
if self._branch.revision_history()[-1] != self._last_revid:
279
236
def use_cache(self, cache):
280
237
self._change_cache = cache
282
def use_file_cache(self, cache):
283
self._file_change_cache = cache
285
239
def use_search_index(self, index):
286
240
self._index = index
289
def has_revisions(self):
290
return not bzrlib.revision.is_null(self.last_revid)
292
242
@with_branch_lock
293
243
def detach(self):
294
244
# called when a new history object needs to be created, because the
327
283
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
286
def get_sequence(self, revid):
287
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
330
290
def get_revision_history(self):
331
291
return self._full_history
333
def get_revids_from(self, revid_list, start_revid):
335
Yield the mainline (wrt start_revid) revisions that merged each
338
if revid_list is None:
339
revid_list = self._full_history
340
revid_set = set(revid_list)
342
def introduced_revisions(revid):
344
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
346
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
347
r.add(self._merge_sort[i][1])
351
if bzrlib.revision.is_null(revid):
353
if introduced_revisions(revid) & revid_set:
293
def get_revid_sequence(self, revid_list, revid):
295
given a list of revision ids, return the sequence # of this revid in
304
def get_revids_from(self, revid_list, revid):
306
given a list of revision ids, yield revisions in graph order,
307
starting from revid. the list can be None if you just want to travel
308
across all revisions.
311
if (revid_list is None) or (revid in revid_list):
313
if not self._revision_graph.has_key(revid):
355
315
parents = self._revision_graph[revid]
356
316
if len(parents) == 0:
358
318
revid = parents[0]
360
320
@with_branch_lock
361
321
def get_short_revision_history_by_fileid(self, file_id):
362
322
# wow. is this really the only way we can get this list? by
471
429
if self.revno_re.match(revid):
472
430
revid = self._revno_revid[revid]
475
433
@with_branch_lock
476
434
def get_file_view(self, revid, file_id):
478
Given a revid and optional path, return a (revlist, revid) for
479
navigation through the current scope: from the revid (or the latest
480
revision) back to the original revision.
436
Given an optional revid and optional path, return a (revlist, revid)
437
for navigation through the current scope: from the revid (or the
438
latest revision) back to the original revision.
482
440
If file_id is None, the entire revision history is the list scope.
441
If revid is None, the latest revision is used.
484
443
if revid is None:
485
444
revid = self._last_revid
486
445
if file_id is not None:
487
# since revid is 'start_revid', possibly should start the path
488
# tracing from revid... FIXME
446
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
447
inv = self._branch.repository.get_revision_inventory(revid)
489
448
revlist = list(self.get_short_revision_history_by_fileid(file_id))
490
449
revlist = list(self.get_revids_from(revlist, revid))
492
451
revlist = list(self.get_revids_from(None, revid))
454
return revlist, revid
495
456
@with_branch_lock
496
457
def get_view(self, revid, start_revid, file_id, query=None):
498
459
use the URL parameters (revid, start_revid, file_id, and query) to
499
460
determine the revision list we're viewing (start_revid, file_id, query)
500
461
and where we are in it (revid).
502
- if a query is given, we're viewing query results.
503
- if a file_id is given, we're viewing revisions for a specific
505
- if a start_revid is given, we're viewing the branch from a
506
specific revision up the tree.
508
these may be combined to view revisions for a specific file, from
509
a specific revision, with a specific search query.
511
returns a new (revid, start_revid, revid_list) where:
463
if a query is given, we're viewing query results.
464
if a file_id is given, we're viewing revisions for a specific file.
465
if a start_revid is given, we're viewing the branch from a
466
specific revision up the tree.
467
(these may be combined to view revisions for a specific file, from
468
a specific revision, with a specific search query.)
470
returns a new (revid, start_revid, revid_list, scan_list) where:
513
472
- revid: current position within the view
514
473
- start_revid: starting revision of this view
515
474
- revid_list: list of revision ids for this view
517
476
file_id and query are never changed so aren't returned, but they may
518
477
contain vital context for future url navigation.
520
if start_revid is None:
521
start_revid = self._last_revid
523
479
if query is None:
524
revid_list = self.get_file_view(start_revid, file_id)
480
revid_list, start_revid = self.get_file_view(start_revid, file_id)
525
481
if revid is None:
526
482
revid = start_revid
527
483
if revid not in revid_list:
528
484
# if the given revid is not in the revlist, use a revlist that
529
485
# starts at the given revid.
530
revid_list = self.get_file_view(revid, file_id)
486
revid_list, start_revid = self.get_file_view(revid, file_id)
532
487
return revid, start_revid, revid_list
534
489
# potentially limit the search
535
if file_id is not None:
536
revid_list = self.get_file_view(start_revid, file_id)
490
if (start_revid is not None) or (file_id is not None):
491
revid_list, start_revid = self.get_file_view(start_revid, file_id)
538
493
revid_list = None
638
598
p.branch_nick = p_change_dict[p.revid].branch_nick
640
600
p.branch_nick = '(missing)'
642
602
@with_branch_lock
643
def get_changes(self, revid_list):
644
"""Return a list of changes objects for the given revids.
646
Revisions not present and NULL_REVISION will be ignored.
603
def get_changes(self, revid_list, get_diffs=False):
648
604
if self._change_cache is None:
649
changes = self.get_changes_uncached(revid_list)
605
changes = self.get_changes_uncached(revid_list, get_diffs)
651
changes = self._change_cache.get_changes(revid_list)
652
if len(changes) == 0:
607
changes = self._change_cache.get_changes(revid_list, get_diffs)
655
611
# some data needs to be recalculated each time, because it may
656
612
# change as new revisions are added.
657
for change in changes:
658
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
613
for i in xrange(len(revid_list)):
614
revid = revid_list[i]
616
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
659
617
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
660
if len(change.parents) > 0:
661
if isinstance(change.parents[0], util.Container):
662
# old cache stored a potentially-bogus revno
663
change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
665
change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
666
change.revno = self.get_revno(change.revid)
669
for change in changes:
670
change.parity = parity
675
# alright, let's profile this sucka. (FIXME remove this eventually...)
676
def _get_changes_profiled(self, revid_list):
621
# alright, let's profile this sucka.
622
def _get_changes_profiled(self, revid_list, get_diffs=False):
677
623
from loggerhead.lsprof import profile
679
ret, stats = profile(self.get_changes_uncached, revid_list)
625
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
682
628
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
683
629
self.log.info('lsprof complete!')
687
@with_bzrlib_read_lock
688
def get_changes_uncached(self, revid_list):
689
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
691
repo = self._branch.repository
692
parent_map = repo.get_graph().get_parent_map(revid_list)
693
# We need to return the answer in the same order as the input,
695
present_revids = [revid for revid in revid_list
696
if revid in parent_map]
697
rev_list = repo.get_revisions(present_revids)
699
return [self._change_from_revision(rev) for rev in rev_list]
701
632
def _get_deltas_for_revisions_with_trees(self, revisions):
702
"""Produce a list of revision deltas.
633
"""Produce a generator of revision deltas.
704
635
Note that the input is a sequence of REVISIONS, not revision_ids.
705
636
Trees will be held in memory until the generator exits.
706
637
Each delta is relative to the revision's lefthand predecessor.
707
(This is copied from bzrlib.)
709
639
required_trees = set()
710
640
for revision in revisions:
711
required_trees.add(revision.revid)
712
required_trees.update([p.revid for p in revision.parents[:1]])
713
trees = dict((t.get_revision_id(), t) for
641
required_trees.add(revision.revision_id)
642
required_trees.update(revision.parent_ids[:1])
643
trees = dict((t.get_revision_id(), t) for
714
644
t in self._branch.repository.revision_trees(required_trees))
716
646
self._branch.repository.lock_read()
718
648
for revision in revisions:
719
if not revision.parents:
720
old_tree = self._branch.repository.revision_tree(
721
bzrlib.revision.NULL_REVISION)
649
if not revision.parent_ids:
650
old_tree = self._branch.repository.revision_tree(None)
723
old_tree = trees[revision.parents[0].revid]
724
tree = trees[revision.revid]
725
ret.append(tree.changes_from(old_tree))
652
old_tree = trees[revision.parent_ids[0]]
653
tree = trees[revision.revision_id]
654
ret.append((tree, old_tree, tree.changes_from(old_tree)))
728
657
self._branch.repository.unlock()
730
def _change_from_revision(self, revision):
732
Given a bzrlib Revision, return a processed "change" for use in
659
def entry_from_revision(self, revision):
735
660
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
737
662
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
664
if len(parents) == 0:
667
left_parent = revision.parent_ids[0]
739
669
message, short_message = clean_message(revision.message)
742
672
'revid': revision.revision_id,
673
'revno': self.get_revno(revision.revision_id),
743
674
'date': commit_time,
744
675
'author': revision.committer,
745
676
'branch_nick': revision.properties.get('branch-nick', None),
746
677
'short_comment': short_message,
747
678
'comment': revision.message,
748
679
'comment_clean': [util.html_clean(s) for s in message],
749
'parents': revision.parent_ids,
751
682
return util.Container(entry)
753
def get_file_changes_uncached(self, entries):
754
delta_list = self._get_deltas_for_revisions_with_trees(entries)
756
return [self.parse_delta(delta) for delta in delta_list]
759
def get_file_changes(self, entries):
760
if self._file_change_cache is None:
761
return self.get_file_changes_uncached(entries)
763
return self._file_change_cache.get_file_changes(entries)
765
def add_changes(self, entries):
766
changes_list = self.get_file_changes(entries)
768
for entry, changes in zip(entries, changes_list):
769
entry.changes = changes
772
def get_change_with_diff(self, revid, compare_revid=None):
773
change = self.get_changes([revid])[0]
775
if compare_revid is None:
777
compare_revid = change.parents[0].revid
779
compare_revid = 'null:'
781
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
782
rev_tree2 = self._branch.repository.revision_tree(revid)
685
@with_bzrlib_read_lock
686
def get_changes_uncached(self, revid_list, get_diffs=False):
690
rev_list = self._branch.repository.get_revisions(revid_list)
692
except (KeyError, bzrlib.errors.NoSuchRevision), e:
693
# this sometimes happens with arch-converted branches.
694
# i don't know why. :(
695
self.log.debug('No such revision (skipping): %s', e)
696
revid_list.remove(e.revision)
698
delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
699
combined_list = zip(rev_list, delta_list)
702
for rev, (new_tree, old_tree, delta) in combined_list:
703
entry = self.entry_from_revision(rev)
704
entry.changes = self.parse_delta(delta, get_diffs, old_tree, new_tree)
705
entries.append(entry)
709
@with_bzrlib_read_lock
710
def _get_diff(self, revid1, revid2):
711
rev_tree1 = self._branch.repository.revision_tree(revid1)
712
rev_tree2 = self._branch.repository.revision_tree(revid2)
783
713
delta = rev_tree2.changes_from(rev_tree1)
785
change.changes = self.parse_delta(delta)
786
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
714
return rev_tree1, rev_tree2, delta
716
def get_diff(self, revid1, revid2):
717
rev_tree1, rev_tree2, delta = self._get_diff(revid1, revid2)
718
entry = self.get_changes([ revid2 ], False)[0]
719
entry.changes = self.parse_delta(delta, True, rev_tree1, rev_tree2)
790
722
@with_branch_lock
791
723
def get_file(self, file_id, revid):
792
724
"returns (path, filename, data)"
754
if C{get_diffs} is false, the C{chunks} will be omitted.
821
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
823
process.append((old_path, new_path, fid, kind))
824
for path, fid, kind, text_modified, meta_modified in delta.modified:
825
process.append((path, path, fid, kind))
827
for old_path, new_path, fid, kind in process:
761
def rich_filename(path, kind):
762
if kind == 'directory':
764
if kind == 'symlink':
768
def process_diff(diff):
771
for line in diff.splitlines():
774
if line.startswith('+++ ') or line.startswith('--- '):
776
if line.startswith('@@ '):
778
if chunk is not None:
780
chunk = util.Container()
782
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
783
old_lineno = lines[0]
784
new_lineno = lines[1]
785
elif line.startswith(' '):
786
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
787
type='context', line=util.html_clean(line[1:])))
790
elif line.startswith('+'):
791
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
792
type='insert', line=util.html_clean(line[1:])))
794
elif line.startswith('-'):
795
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
796
type='delete', line=util.html_clean(line[1:])))
799
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
800
type='unknown', line=util.html_clean(repr(line))))
801
if chunk is not None:
805
def handle_modify(old_path, new_path, fid, kind):
807
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
828
809
old_lines = old_tree.get_file_lines(fid)
829
810
new_lines = new_tree.get_file_lines(fid)
830
811
buffer = StringIO()
831
if old_lines != new_lines:
833
bzrlib.diff.internal_diff(old_path, old_lines,
834
new_path, new_lines, buffer)
835
except bzrlib.errors.BinaryFile:
838
diff = buffer.getvalue()
813
bzrlib.diff.internal_diff(old_path, old_lines,
814
new_path, new_lines, buffer)
815
except bzrlib.errors.BinaryFile:
841
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
845
def _process_diff(self, diff):
846
# doesn't really need to be a method; could be static.
849
for line in diff.splitlines():
852
if line.startswith('+++ ') or line.startswith('--- '):
854
if line.startswith('@@ '):
856
if chunk is not None:
858
chunk = util.Container()
860
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
861
old_lineno = lines[0]
862
new_lineno = lines[1]
863
elif line.startswith(' '):
864
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
865
type='context', line=util.fixed_width(line[1:])))
868
elif line.startswith('+'):
869
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
870
type='insert', line=util.fixed_width(line[1:])))
872
elif line.startswith('-'):
873
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
874
type='delete', line=util.fixed_width(line[1:])))
877
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
878
type='unknown', line=util.fixed_width(repr(line))))
879
if chunk is not None:
883
def parse_delta(self, delta):
885
Return a nested data structure containing the changes in a delta::
887
added: list((filename, file_id)),
888
renamed: list((old_filename, new_filename, file_id)),
889
deleted: list((filename, file_id)),
818
diff = buffer.getvalue()
819
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
900
821
for path, fid, kind in delta.added:
901
822
added.append((rich_filename(path, kind), fid))
903
824
for path, fid, kind, text_modified, meta_modified in delta.modified:
904
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
906
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
907
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
825
handle_modify(path, path, fid, kind)
827
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
828
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
908
829
if meta_modified or text_modified:
909
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
830
handle_modify(oldpath, newpath, fid, kind)
911
832
for path, fid, kind in delta.removed:
912
833
removed.append((rich_filename(path, kind), fid))
914
835
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
919
840
for change in changes:
920
841
for m in change.changes.modified:
921
842
m.sbs_chunks = _make_side_by_side(m.chunks)
923
844
@with_branch_lock
924
def get_filelist(self, inv, file_id, sort_type=None):
845
def get_filelist(self, inv, path, sort_type=None):
926
847
return the list of all files (and their attributes) within a given
930
dir_ie = inv[file_id]
931
path = inv.id2path(file_id)
850
while path.endswith('/'):
852
if path.startswith('/'):
855
entries = inv.entries()
936
for filename, entry in dir_ie.children.iteritems():
937
revid_set.add(entry.revision)
940
for change in self.get_changes(list(revid_set)):
941
change_dict[change.revid] = change
943
for filename, entry in dir_ie.children.iteritems():
858
for filepath, entry in entries:
859
if posixpath.dirname(filepath) != path:
861
filename = posixpath.basename(filepath)
862
rich_filename = filename
944
863
pathname = filename
945
864
if entry.kind == 'directory':
948
867
revid = entry.revision
868
revision = self._branch.repository.get_revision(revid)
950
file = util.Container(
951
filename=filename, executable=entry.executable, kind=entry.kind,
952
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
953
revid=revid, change=change_dict[revid])
870
change = util.Container(date=datetime.datetime.fromtimestamp(revision.timestamp),
871
revno=self.get_revno(revid))
873
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
874
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
954
875
file_list.append(file)
956
if sort_type == 'filename' or sort_type is None:
877
if sort_type == 'filename':
957
878
file_list.sort(key=lambda x: x.filename)
958
879
elif sort_type == 'size':
959
880
file_list.sort(key=lambda x: x.size)
960
881
elif sort_type == 'date':
961
882
file_list.sort(key=lambda x: x.change.date)
964
885
for file in file_list:
965
886
file.parity = parity