144
133
def clean_message(message):
145
# clean up a commit message and return it and a short (1-line) version
134
"""Clean up a commit message and return it and a short (1-line) version.
136
Commit messages that are long single lines are reflowed using the textwrap
137
module (Robey, the original author of this code, apparently favored this
146
140
message = message.splitlines()
147
142
if len(message) == 1:
148
# robey-style 1-line long message
149
143
message = textwrap.wrap(message[0])
150
elif len(message) == 0:
151
# sometimes a commit may have NO message!
154
# make short form of commit message
145
if len(message) == 0:
146
# We can end up where when (a) the commit message was empty or (b)
147
# when the message consisted entirely of whitespace, in which case
148
# textwrap.wrap() returns an empty list.
151
# Make short form of commit message.
155
152
short_message = message[0]
156
if len(short_message) > 60:
157
short_message = short_message[:60] + '...'
153
if len(short_message) > 80:
154
short_message = short_message[:80] + '...'
159
156
return message, short_message
159
def rich_filename(path, kind):
160
if kind == 'directory':
162
if kind == 'symlink':
163
169
class _RevListToTimestamps(object):
164
170
"""This takes a list of revisions, and allows you to bisect by date"""
180
186
class History (object):
182
188
def __init__(self):
183
189
self._change_cache = None
190
self._file_change_cache = None
184
191
self._index = None
185
192
self._lock = threading.RLock()
188
195
def from_branch(cls, branch, name=None):
191
198
self._branch = branch
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)
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 = {}
197
206
name = self._branch.nick
198
207
self._name = name
199
208
self.log = logging.getLogger('loggerhead.%s' % (name,))
201
210
self._full_history = []
202
211
self._revision_info = {}
203
212
self._revno_revid = {}
204
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
206
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
207
215
self._full_history.append(revid)
208
216
revno_str = '.'.join(str(n) for n in revno)
209
217
self._revno_revid[revno_str] = revid
210
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
214
220
# cache merge info
215
221
self._where_merged = {}
216
222
for revid in self._revision_graph.keys():
217
if not revid in self._full_history:
223
if not revid in self._full_history:
219
225
for parent in self._revision_graph[revid]:
220
226
self._where_merged.setdefault(parent, set()).add(revid)
222
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
226
232
def from_folder(cls, path, name=None):
227
233
b = bzrlib.branch.Branch.open(path)
429
421
if self.revno_re.match(revid):
430
422
revid = self._revno_revid[revid]
433
425
@with_branch_lock
434
426
def get_file_view(self, revid, file_id):
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.
428
Given a revid and optional path, return a (revlist, revid) for
429
navigation through the current scope: from the revid (or the latest
430
revision) back to the original revision.
440
432
If file_id is None, the entire revision history is the list scope.
441
If revid is None, the latest revision is used.
443
434
if revid is None:
444
435
revid = self._last_revid
445
436
if file_id is not None:
446
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
447
inv = self._branch.repository.get_revision_inventory(revid)
437
# since revid is 'start_revid', possibly should start the path
438
# tracing from revid... FIXME
448
439
revlist = list(self.get_short_revision_history_by_fileid(file_id))
449
440
revlist = list(self.get_revids_from(revlist, revid))
451
442
revlist = list(self.get_revids_from(None, revid))
454
return revlist, revid
456
445
@with_branch_lock
457
446
def get_view(self, revid, start_revid, file_id, query=None):
459
448
use the URL parameters (revid, start_revid, file_id, and query) to
460
449
determine the revision list we're viewing (start_revid, file_id, query)
461
450
and where we are in it (revid).
463
452
if a query is given, we're viewing query results.
464
453
if a file_id is given, we're viewing revisions for a specific file.
465
454
if a start_revid is given, we're viewing the branch from a
466
455
specific revision up the tree.
467
456
(these may be combined to view revisions for a specific file, from
468
457
a specific revision, with a specific search query.)
470
459
returns a new (revid, start_revid, revid_list, scan_list) where:
472
461
- revid: current position within the view
473
462
- start_revid: starting revision of this view
474
463
- revid_list: list of revision ids for this view
476
465
file_id and query are never changed so aren't returned, but they may
477
466
contain vital context for future url navigation.
468
if start_revid is None:
469
start_revid = self._last_revid
479
471
if query is None:
480
revid_list, start_revid = self.get_file_view(start_revid, file_id)
472
revid_list = self.get_file_view(start_revid, file_id)
481
473
if revid is None:
482
474
revid = start_revid
483
475
if revid not in revid_list:
484
476
# if the given revid is not in the revlist, use a revlist that
485
477
# starts at the given revid.
486
revid_list, start_revid = self.get_file_view(revid, file_id)
478
revid_list= self.get_file_view(revid, file_id)
487
480
return revid, start_revid, revid_list
489
482
# potentially limit the search
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)
483
if file_id is not None:
484
revid_list = self.get_file_view(start_revid, file_id)
493
486
revid_list = None
629
621
self.log.info('lsprof complete!')
632
def _get_deltas_for_revisions_with_trees(self, revisions):
624
def _get_deltas_for_revisions_with_trees(self, entries):
633
625
"""Produce a generator of revision deltas.
635
627
Note that the input is a sequence of REVISIONS, not revision_ids.
636
628
Trees will be held in memory until the generator exits.
637
629
Each delta is relative to the revision's lefthand predecessor.
639
631
required_trees = set()
640
for revision in revisions:
641
required_trees.add(revision.revision_id)
642
required_trees.update(revision.parent_ids[:1])
643
trees = dict((t.get_revision_id(), t) for
632
for entry in entries:
633
required_trees.add(entry.revid)
634
required_trees.update([p.revid for p in entry.parents[:1]])
635
trees = dict((t.get_revision_id(), t) for
644
636
t in self._branch.repository.revision_trees(required_trees))
646
638
self._branch.repository.lock_read()
648
for revision in revisions:
649
if not revision.parent_ids:
650
old_tree = self._branch.repository.revision_tree(None)
640
for entry in entries:
641
if not entry.parents:
642
old_tree = self._branch.repository.revision_tree(
643
bzrlib.revision.NULL_REVISION)
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)))
645
old_tree = trees[entry.parents[0].revid]
646
tree = trees[entry.revid]
647
ret.append(tree.changes_from(old_tree))
657
650
self._branch.repository.unlock()
659
652
def entry_from_revision(self, revision):
660
653
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
662
655
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]
669
657
message, short_message = clean_message(revision.message)
672
660
'revid': revision.revision_id,
673
'revno': self.get_revno(revision.revision_id),
674
661
'date': commit_time,
675
662
'author': revision.committer,
676
663
'branch_nick': revision.properties.get('branch-nick', None),
682
669
return util.Container(entry)
684
671
@with_branch_lock
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)
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()
692
def get_file_changes_uncached(self, entries):
693
delta_list = self._get_deltas_for_revisions_with_trees(entries)
695
return [self.parse_delta(delta) for delta in delta_list]
698
def get_file_changes(self, entries):
699
if self._file_change_cache is None:
700
return self.get_file_changes_uncached(entries)
702
return self._file_change_cache.get_file_changes(entries)
704
def add_changes(self, entries):
705
changes_list = self.get_file_changes(entries)
707
for entry, changes in zip(entries, changes_list):
708
entry.changes = changes
711
def get_change_with_diff(self, revid, compare_revid=None):
712
entry = self.get_changes([revid])[0]
714
if compare_revid is None:
716
compare_revid = entry.parents[0].revid
718
compare_revid = 'null:'
720
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
721
rev_tree2 = self._branch.repository.revision_tree(revid)
713
722
delta = rev_tree2.changes_from(rev_tree1)
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)
724
entry.changes = self.parse_delta(delta)
726
entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
722
730
@with_branch_lock
723
731
def get_file(self, file_id, revid):
724
732
"returns (path, filename, data)"
754
if C{get_diffs} is false, the C{chunks} will be omitted.
761
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
763
process.append((old_path, new_path, fid, kind))
764
for path, fid, kind, text_modified, meta_modified in delta.modified:
765
process.append((path, path, fid, kind))
767
for old_path, new_path, fid, kind in process:
768
old_lines = old_tree.get_file_lines(fid)
769
new_lines = new_tree.get_file_lines(fid)
771
if old_lines != new_lines:
773
bzrlib.diff.internal_diff(old_path, old_lines,
774
new_path, new_lines, buffer)
775
except bzrlib.errors.BinaryFile:
778
diff = buffer.getvalue()
781
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
785
def _process_diff(self, diff):
786
# doesn't really need to be a method; could be static.
789
for line in diff.splitlines():
792
if line.startswith('+++ ') or line.startswith('--- '):
794
if line.startswith('@@ '):
796
if chunk is not None:
798
chunk = util.Container()
800
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
801
old_lineno = lines[0]
802
new_lineno = lines[1]
803
elif line.startswith(' '):
804
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
805
type='context', line=util.fixed_width(line[1:])))
808
elif line.startswith('+'):
809
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
810
type='insert', line=util.fixed_width(line[1:])))
812
elif line.startswith('-'):
813
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
814
type='delete', line=util.fixed_width(line[1:])))
817
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
818
type='unknown', line=util.fixed_width(repr(line))))
819
if chunk is not None:
823
def parse_delta(self, delta):
825
Return a nested data structure containing the changes in a delta::
827
added: list((filename, file_id)),
828
renamed: list((old_filename, new_filename, file_id)),
829
deleted: list((filename, file_id)),
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))
809
old_lines = old_tree.get_file_lines(fid)
810
new_lines = new_tree.get_file_lines(fid)
813
bzrlib.diff.internal_diff(old_path, old_lines,
814
new_path, new_lines, buffer)
815
except bzrlib.errors.BinaryFile:
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))
821
840
for path, fid, kind in delta.added:
822
841
added.append((rich_filename(path, kind), fid))
824
843
for path, fid, kind, text_modified, meta_modified in delta.modified:
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))
844
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
846
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
847
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
829
848
if meta_modified or text_modified:
830
handle_modify(oldpath, newpath, fid, kind)
849
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
832
851
for path, fid, kind in delta.removed:
833
852
removed.append((rich_filename(path, kind), fid))
835
854
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
840
859
for change in changes:
841
860
for m in change.changes.modified:
842
861
m.sbs_chunks = _make_side_by_side(m.chunks)
844
863
@with_branch_lock
845
def get_filelist(self, inv, path, sort_type=None):
864
def get_filelist(self, inv, file_id, sort_type=None):
847
866
return the list of all files (and their attributes) within a given
850
while path.endswith('/'):
852
if path.startswith('/'):
855
entries = inv.entries()
858
for filepath, entry in entries:
859
fetch_set.add(entry.revision)
860
change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
870
dir_ie = inv[file_id]
871
path = inv.id2path(file_id)
863
for filepath, entry in entries:
864
if posixpath.dirname(filepath) != path:
866
filename = posixpath.basename(filepath)
867
rich_filename = filename
876
for filename, entry in dir_ie.children.iteritems():
877
revid_set.add(entry.revision)
880
for change in self.get_changes(list(revid_set)):
881
change_dict[change.revid] = change
883
for filename, entry in dir_ie.children.iteritems():
868
884
pathname = filename
869
885
if entry.kind == 'directory':
873
888
revid = entry.revision
874
change = change_dict[revid]
876
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
877
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
890
file = util.Container(
891
filename=filename, executable=entry.executable, kind=entry.kind,
892
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
893
revid=revid, change=change_dict[revid])
878
894
file_list.append(file)
880
if sort_type == 'filename':
896
if sort_type == 'filename' or sort_type is None:
881
897
file_list.sort(key=lambda x: x.filename)
882
898
elif sort_type == 'size':
883
899
file_list.sort(key=lambda x: x.size)
884
900
elif sort_type == 'date':
885
901
file_list.sort(key=lambda x: x.change.date)
888
904
for file in file_list:
889
905
file.parity = parity