81
72
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
75
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
76
while len(delete_list) < len(insert_list):
77
delete_list.append((None, '', 'context'))
78
while len(insert_list) < len(delete_list):
79
insert_list.append((None, '', 'context'))
80
while len(delete_list) > 0:
81
d = delete_list.pop(0)
82
i = insert_list.pop(0)
83
line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
84
old_line=d[1], new_line=i[1],
85
old_type=d[2], new_type=i[2]))
88
def _make_side_by_side(chunk_list):
90
turn a normal unified-style diff (post-processed by parse_delta) into a
91
side-by-side diff structure. the new structure is::
99
type: str('context' or 'changed'),
104
for chunk in chunk_list:
106
delete_list, insert_list = [], []
107
for line in chunk.diff:
108
if line.type == 'context':
109
if len(delete_list) or len(insert_list):
110
_process_side_by_side_buffers(line_list, delete_list, insert_list)
111
delete_list, insert_list = [], []
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
elif line.type == 'delete':
116
delete_list.append((line.old_lineno, line.line, line.type))
117
elif line.type == 'insert':
118
insert_list.append((line.new_lineno, line.line, line.type))
119
if len(delete_list) or len(insert_list):
120
_process_side_by_side_buffers(line_list, delete_list, insert_list)
121
out_chunk_list.append(util.Container(diff=line_list))
122
return out_chunk_list
125
def is_branch(folder):
127
bzrlib.branch.Branch.open(folder)
133
def clean_message(message):
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
140
message = message.splitlines()
142
if len(message) == 1:
143
message = textwrap.wrap(message[0])
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.
152
short_message = message[0]
153
if len(short_message) > 80:
154
short_message = short_message[:80] + '...'
156
return message, short_message
159
def rich_filename(path, kind):
160
if kind == 'directory':
162
if kind == 'symlink':
85
169
class _RevListToTimestamps(object):
86
170
"""This takes a list of revisions, and allows you to bisect by date"""
102
186
class History (object):
104
188
def __init__(self):
105
189
self._change_cache = None
190
self._file_change_cache = None
106
191
self._index = None
107
192
self._lock = threading.RLock()
110
195
def from_branch(cls, branch, name=None):
113
198
self._branch = branch
114
self._history = branch.revision_history()
115
self._last_revid = self._history[-1]
116
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 = {}
119
206
name = self._branch.nick
120
207
self._name = name
121
208
self.log = logging.getLogger('loggerhead.%s' % (name,))
123
210
self._full_history = []
124
211
self._revision_info = {}
125
212
self._revno_revid = {}
126
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
128
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
129
215
self._full_history.append(revid)
130
216
revno_str = '.'.join(str(n) for n in revno)
131
217
self._revno_revid[revno_str] = revid
132
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
136
220
# cache merge info
137
221
self._where_merged = {}
138
222
for revid in self._revision_graph.keys():
139
if not revid in self._full_history:
223
if not revid in self._full_history:
141
225
for parent in self._revision_graph[revid]:
142
226
self._where_merged.setdefault(parent, set()).add(revid)
144
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
148
232
def from_folder(cls, path, name=None):
149
233
b = bzrlib.branch.Branch.open(path)
341
416
# if a "revid" is actually a dotted revno, convert it to a revid
342
417
if revid is None:
420
return self._last_revid
344
421
if self.revno_re.match(revid):
345
422
revid = self._revno_revid[revid]
348
425
@with_branch_lock
349
426
def get_file_view(self, revid, file_id):
351
Given an optional revid and optional path, return a (revlist, revid)
352
for navigation through the current scope: from the revid (or the
353
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.
355
432
If file_id is None, the entire revision history is the list scope.
356
If revid is None, the latest revision is used.
358
434
if revid is None:
359
435
revid = self._last_revid
360
436
if file_id is not None:
361
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
362
inv = self._branch.repository.get_revision_inventory(revid)
437
# since revid is 'start_revid', possibly should start the path
438
# tracing from revid... FIXME
363
439
revlist = list(self.get_short_revision_history_by_fileid(file_id))
364
440
revlist = list(self.get_revids_from(revlist, revid))
366
442
revlist = list(self.get_revids_from(None, revid))
369
return revlist, revid
371
445
@with_branch_lock
372
446
def get_view(self, revid, start_revid, file_id, query=None):
374
448
use the URL parameters (revid, start_revid, file_id, and query) to
375
449
determine the revision list we're viewing (start_revid, file_id, query)
376
450
and where we are in it (revid).
378
452
if a query is given, we're viewing query results.
379
453
if a file_id is given, we're viewing revisions for a specific file.
380
454
if a start_revid is given, we're viewing the branch from a
381
455
specific revision up the tree.
382
456
(these may be combined to view revisions for a specific file, from
383
457
a specific revision, with a specific search query.)
385
459
returns a new (revid, start_revid, revid_list, scan_list) where:
387
461
- revid: current position within the view
388
462
- start_revid: starting revision of this view
389
463
- revid_list: list of revision ids for this view
391
465
file_id and query are never changed so aren't returned, but they may
392
466
contain vital context for future url navigation.
468
if start_revid is None:
469
start_revid = self._last_revid
394
471
if query is None:
395
revid_list, start_revid = self.get_file_view(start_revid, file_id)
472
revid_list = self.get_file_view(start_revid, file_id)
396
473
if revid is None:
397
474
revid = start_revid
398
475
if revid not in revid_list:
399
476
# if the given revid is not in the revlist, use a revlist that
400
477
# starts at the given revid.
401
revid_list, start_revid = self.get_file_view(revid, file_id)
478
revid_list= self.get_file_view(revid, file_id)
402
480
return revid, start_revid, revid_list
404
482
# potentially limit the search
405
if (start_revid is not None) or (file_id is not None):
406
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)
408
486
revid_list = None
496
575
p_changes = self.get_changes(list(fetch_set))
497
576
p_change_dict = dict([(c.revid, c) for c in p_changes])
498
577
for change in changes:
578
# arch-converted branches may not have merged branch info :(
499
579
for p in change.parents:
500
p.branch_nick = p_change_dict[p.revid].branch_nick
580
if p.revid in p_change_dict:
581
p.branch_nick = p_change_dict[p.revid].branch_nick
583
p.branch_nick = '(missing)'
501
584
for p in change.merge_points:
502
p.branch_nick = p_change_dict[p.revid].branch_nick
585
if p.revid in p_change_dict:
586
p.branch_nick = p_change_dict[p.revid].branch_nick
588
p.branch_nick = '(missing)'
504
590
@with_branch_lock
505
def get_changes(self, revid_list, get_diffs=False):
591
def get_changes(self, revid_list):
506
592
if self._change_cache is None:
507
changes = self.get_changes_uncached(revid_list, get_diffs)
593
changes = self.get_changes_uncached(revid_list)
509
changes = self._change_cache.get_changes(revid_list, get_diffs)
595
changes = self._change_cache.get_changes(revid_list)
596
if len(changes) == 0:
513
599
# some data needs to be recalculated each time, because it may
514
600
# change as new revisions are added.
515
for i in xrange(len(revid_list)):
516
revid = revid_list[i]
518
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
601
for change in changes:
602
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
519
603
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
604
change.revno = self.get_revno(change.revid)
607
for change in changes:
608
change.parity = parity
523
613
# alright, let's profile this sucka.
530
620
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
621
self.log.info('lsprof complete!')
534
@with_bzrlib_read_lock
535
def get_changes_uncached(self, revid_list, get_diffs=False):
537
rev_list = self._branch.repository.get_revisions(revid_list)
538
except (KeyError, bzrlib.errors.NoSuchRevision):
541
delta_list = self._branch.repository.get_deltas_for_revisions(rev_list)
542
combined_list = zip(rev_list, delta_list)
546
# lookup the trees for each revision, so we can calculate diffs
549
lookup_set.add(rev.revision_id)
550
if len(rev.parent_ids) > 0:
551
lookup_set.add(rev.parent_ids[0])
552
tree_map = dict((t.get_revision_id(), t) for t in self._branch.repository.revision_trees(lookup_set))
553
# also the root tree, in case we hit the origin:
554
tree_map[None] = self._branch.repository.revision_tree(None)
557
for rev, delta in combined_list:
558
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
560
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
562
if len(parents) == 0:
624
def _get_deltas_for_revisions_with_trees(self, entries):
625
"""Produce a generator of revision deltas.
627
Note that the input is a sequence of REVISIONS, not revision_ids.
628
Trees will be held in memory until the generator exits.
629
Each delta is relative to the revision's lefthand predecessor.
631
required_trees = set()
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
636
t in self._branch.repository.revision_trees(required_trees))
638
self._branch.repository.lock_read()
640
for entry in entries:
641
if not entry.parents:
642
old_tree = self._branch.repository.revision_tree(
643
bzrlib.revision.NULL_REVISION)
645
old_tree = trees[entry.parents[0].revid]
646
tree = trees[entry.revid]
647
ret.append(tree.changes_from(old_tree))
650
self._branch.repository.unlock()
652
def entry_from_revision(self, revision):
653
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
655
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
657
message, short_message = clean_message(revision.message)
660
'revid': revision.revision_id,
662
'author': revision.committer,
663
'branch_nick': revision.properties.get('branch-nick', None),
664
'short_comment': short_message,
665
'comment': revision.message,
666
'comment_clean': [util.html_clean(s) for s in message],
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()
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
565
left_parent = rev.parent_ids[0]
567
message = rev.message.splitlines()
568
if len(message) == 1:
569
# robey-style 1-line long message
570
message = textwrap.wrap(message[0])
572
# make short form of commit message
573
short_message = message[0]
574
if len(short_message) > 60:
575
short_message = short_message[:60] + '...'
577
old_tree, new_tree = None, None
579
new_tree = tree_map[rev.revision_id]
580
old_tree = tree_map[left_parent]
583
'revid': rev.revision_id,
584
'revno': self.get_revno(rev.revision_id),
586
'author': rev.committer,
587
'branch_nick': rev.properties.get('branch-nick', None),
588
'short_comment': short_message,
589
'comment': rev.message,
590
'comment_clean': [util.html_clean(s) for s in message],
592
'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
594
entries.append(util.Container(entry))
718
compare_revid = 'null:'
720
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
721
rev_tree2 = self._branch.repository.revision_tree(revid)
722
delta = rev_tree2.changes_from(rev_tree1)
724
entry.changes = self.parse_delta(delta)
726
entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
598
730
@with_branch_lock
599
731
def get_file(self, file_id, revid):
600
"returns (filename, data)"
601
inv_entry = self.get_inventory(revid)[file_id]
732
"returns (path, filename, data)"
733
inv = self.get_inventory(revid)
734
inv_entry = inv[file_id]
602
735
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
603
return inv_entry.name, rev_tree.get_file_text(file_id)
606
def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
736
path = inv.id2path(file_id)
737
if not path.startswith('/'):
739
return path, inv_entry.name, rev_tree.get_file_text(file_id)
741
def _parse_diffs(self, old_tree, new_tree, delta):
608
Return a nested data structure containing the changes in a delta::
610
added: list((filename, file_id)),
611
renamed: list((old_filename, new_filename, file_id)),
612
deleted: list((filename, file_id)),
743
Return a list of processed diffs, in the format::
626
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)),
633
def rich_filename(path, kind):
634
if kind == 'directory':
636
if kind == 'symlink':
640
def process_diff(diff):
643
for line in diff.splitlines():
646
if line.startswith('+++ ') or line.startswith('--- '):
648
if line.startswith('@@ '):
650
if chunk is not None:
652
chunk = util.Container()
654
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
655
old_lineno = lines[0]
656
new_lineno = lines[1]
657
elif line.startswith(' '):
658
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
659
type='context', line=util.html_clean(line[1:])))
662
elif line.startswith('+'):
663
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
664
type='insert', line=util.html_clean(line[1:])))
666
elif line.startswith('-'):
667
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
668
type='delete', line=util.html_clean(line[1:])))
671
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
672
type='unknown', line=util.html_clean(repr(line))))
673
if chunk is not None:
677
def handle_modify(old_path, new_path, fid, kind):
679
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
681
old_lines = old_tree.get_file_lines(fid)
682
new_lines = new_tree.get_file_lines(fid)
684
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
685
diff = buffer.getvalue()
686
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
688
840
for path, fid, kind in delta.added:
689
841
added.append((rich_filename(path, kind), fid))
691
843
for path, fid, kind, text_modified, meta_modified in delta.modified:
692
handle_modify(path, path, fid, kind)
694
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
695
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))
696
848
if meta_modified or text_modified:
697
handle_modify(oldpath, newpath, fid, kind)
849
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
699
851
for path, fid, kind in delta.removed:
700
852
removed.append((rich_filename(path, kind), fid))
702
854
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
857
def add_side_by_side(changes):
858
# FIXME: this is a rotten API.
859
for change in changes:
860
for m in change.changes.modified:
861
m.sbs_chunks = _make_side_by_side(m.chunks)
704
863
@with_branch_lock
705
def get_filelist(self, inv, path, sort_type=None):
864
def get_filelist(self, inv, file_id, sort_type=None):
707
866
return the list of all files (and their attributes) within a given
710
while path.endswith('/'):
712
if path.startswith('/'):
715
entries = inv.entries()
718
for filepath, entry in entries:
719
fetch_set.add(entry.revision)
720
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)
723
for filepath, entry in entries:
724
if posixpath.dirname(filepath) != path:
726
filename = posixpath.basename(filepath)
727
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():
728
884
pathname = filename
729
885
if entry.kind == 'directory':
733
888
revid = entry.revision
734
change = change_dict[revid]
736
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
737
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])
738
894
file_list.append(file)
740
if sort_type == 'filename':
896
if sort_type == 'filename' or sort_type is None:
741
897
file_list.sort(key=lambda x: x.filename)
742
898
elif sort_type == 'size':
743
899
file_list.sort(key=lambda x: x.size)
744
900
elif sort_type == 'date':
745
901
file_list.sort(key=lambda x: x.change.date)
748
904
for file in file_list:
749
905
file.parity = parity