39
from StringIO import StringIO
40
import bzrlib.revision
41
42
from loggerhead import search
42
43
from loggerhead import util
43
44
from loggerhead.wholehistory import compute_whole_history_data
49
import bzrlib.progress
50
import bzrlib.revision
51
import bzrlib.textfile
55
# bzrlib's UIFactory is not thread-safe
56
uihack = threading.local()
59
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
61
def nested_progress_bar(self):
62
if getattr(uihack, '_progress_bar_stack', None) is None:
63
pbs = bzrlib.progress.ProgressBarStack(
64
klass=bzrlib.progress.DummyProgress)
65
uihack._progress_bar_stack = pbs
66
return uihack._progress_bar_stack.get_nested()
68
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
70
47
def is_branch(folder):
129
103
def __len__(self):
130
104
return len(self.revid_list)
133
class History (object):
106
class FileChangeReporter(object):
108
def __init__(self, old_inv, new_inv):
113
self.text_changes = []
114
self.old_inv = old_inv
115
self.new_inv = new_inv
117
def revid(self, inv, file_id):
119
return inv[file_id].revision
120
except bzrlib.errors.NoSuchId:
123
def report(self, file_id, paths, versioned, renamed, modified,
125
if modified not in ('unchanged', 'kind changed'):
126
if versioned == 'removed':
127
filename = rich_filename(paths[0], kind[0])
129
filename = rich_filename(paths[1], kind[1])
130
self.text_changes.append(util.Container(
131
filename=filename, file_id=file_id,
132
old_revision=self.revid(self.old_inv, file_id),
133
new_revision=self.revid(self.new_inv, file_id)))
134
if versioned == 'added':
135
self.added.append(util.Container(
136
filename=rich_filename(paths[1], kind),
137
file_id=file_id, kind=kind[1]))
138
elif versioned == 'removed':
139
self.removed.append(util.Container(
140
filename=rich_filename(paths[0], kind),
141
file_id=file_id, kind=kind[0]))
143
self.renamed.append(util.Container(
144
old_filename=rich_filename(paths[0], kind[0]),
145
new_filename=rich_filename(paths[1], kind[1]),
147
text_modified=modified == 'modified'))
149
self.modified.append(util.Container(
150
filename=rich_filename(paths[1], kind),
154
class RevInfoMemoryCache(object):
155
"""A store that validates values against the revids they were stored with.
157
We use a unique key for each branch.
159
The reason for not just using the revid as the key is so that when a new
160
value is provided for a branch, we replace the old value used for the
163
There is another implementation of the same interface in
164
loggerhead.changecache.RevInfoDiskCache.
167
def __init__(self, cache):
170
def get(self, key, revid):
171
"""Return the data associated with `key`, subject to a revid check.
173
If a value was stored under `key`, with the same revid, return it.
174
Otherwise return None.
176
cached = self._cache.get(key)
179
stored_revid, data = cached
180
if revid == stored_revid:
185
def set(self, key, revid, data):
186
"""Store `data` under `key`, to be checked against `revid` on get().
188
self._cache[key] = (revid, data)
191
class History(object):
134
192
"""Decorate a branch to provide information for rendering.
136
194
History objects are expected to be short lived -- when serving a request
138
196
around it, serve the request, throw the History object away, unlock the
139
197
branch and throw it away.
141
:ivar _file_change_cache: xx
199
:ivar _file_change_cache: An object that caches information about the
200
files that changed between two revisions.
201
:ivar _rev_info: A list of information about revisions. This is by far
202
the most cryptic data structure in loggerhead. At the top level, it
203
is a list of 3-tuples [(merge-info, where-merged, parents)].
204
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
205
like a merged sorted list, but the revno is stringified.
206
`where-merged` is a tuple of revisions that have this revision as a
207
non-lefthand parent. Finally, `parents` is just the usual list of
208
parents of this revision.
209
:ivar _rev_indices: A dictionary mapping each revision id to the index of
210
the information about it in _rev_info.
211
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
144
def __init__(self, branch, whole_history_data_cache):
215
def _load_whole_history_data(self, caches, cache_key):
216
"""Set the attributes relating to the whole history of the branch.
218
:param caches: a list of caches with interfaces like
219
`RevInfoMemoryCache` and be ordered from fastest to slowest.
220
:param cache_key: the key to use with the caches.
222
self._rev_indices = None
223
self._rev_info = None
226
def update_missed_caches():
227
for cache in missed_caches:
228
cache.set(cache_key, self.last_revid, self._rev_info)
230
data = cache.get(cache_key, self.last_revid)
232
self._rev_info = data
233
update_missed_caches()
236
missed_caches.append(cache)
238
whole_history_data = compute_whole_history_data(self._branch)
239
self._rev_info, self._rev_indices = whole_history_data
240
update_missed_caches()
242
if self._rev_indices is not None:
243
self._revno_revid = {}
244
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
245
self._revno_revid[revno_str] = revid
247
self._revno_revid = {}
248
self._rev_indices = {}
249
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
250
self._rev_indices[revid] = seq
251
self._revno_revid[revno_str] = revid
253
def __init__(self, branch, whole_history_data_cache, file_cache=None,
254
revinfo_disk_cache=None, cache_key=None):
145
255
assert branch.is_locked(), (
146
256
"Can only construct a History object with a read-locked branch.")
147
self._file_change_cache = None
257
if file_cache is not None:
258
self._file_change_cache = file_cache
259
file_cache.history = self
261
self._file_change_cache = None
148
262
self._branch = branch
149
263
self._inventory_cache = {}
150
264
self._branch_nick = self._branch.get_config().get_nickname()
151
self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
265
self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
153
267
self.last_revid = branch.last_revision()
155
whole_history_data = whole_history_data_cache.get(self.last_revid)
156
if whole_history_data is None:
157
whole_history_data = compute_whole_history_data(branch)
158
whole_history_data_cache[self.last_revid] = whole_history_data
160
(self._revision_graph, self._full_history, self._revision_info,
161
self._revno_revid, self._merge_sort, self._where_merged,
162
) = whole_history_data
164
def use_file_cache(self, cache):
165
self._file_change_cache = cache
269
caches = [RevInfoMemoryCache(whole_history_data_cache)]
270
if revinfo_disk_cache:
271
caches.append(revinfo_disk_cache)
272
self._load_whole_history_data(caches, cache_key)
168
275
def has_revisions(self):
210
318
def get_short_revision_history_by_fileid(self, file_id):
211
319
# FIXME: would be awesome if we could get, for a folder, the list of
212
320
# revisions where items within that folder changed.i
214
# FIXME: Workaround for bzr versions prior to 1.6b3.
215
# Remove me eventually pretty please :)
216
w = self._branch.repository.weave_store.get_weave(
217
file_id, self._branch.repository.get_transaction())
218
w_revids = w.versions()
219
revids = [r for r in self._full_history if r in w_revids]
220
except AttributeError:
221
possible_keys = [(file_id, revid) for revid in self._full_history]
222
get_parent_map = self._branch.repository.texts.get_parent_map
223
# We chunk the requests as this works better with GraphIndex.
224
# See _filter_revisions_touching_file_id in bzrlib/log.py
225
# for more information.
228
for start in xrange(0, len(possible_keys), chunk_size):
229
next_keys = possible_keys[start:start + chunk_size]
230
revids += [k[1] for k in get_parent_map(next_keys)]
231
del possible_keys, next_keys
321
possible_keys = [(file_id, revid) for revid in self._rev_indices]
322
get_parent_map = self._branch.repository.texts.get_parent_map
323
# We chunk the requests as this works better with GraphIndex.
324
# See _filter_revisions_touching_file_id in bzrlib/log.py
325
# for more information.
328
for start in xrange(0, len(possible_keys), chunk_size):
329
next_keys = possible_keys[start:start + chunk_size]
330
revids += [k[1] for k in get_parent_map(next_keys)]
331
del possible_keys, next_keys
234
334
def get_revision_history_since(self, revid_list, date):
448
548
revnol = revno.split(".")
449
549
revnos = ".".join(revnol[:-2])
450
550
revnolast = int(revnol[-1])
451
if revnos in d.keys():
453
553
if revnolast < m:
454
554
d[revnos] = (revnolast, revid)
456
556
d[revnos] = (revnolast, revid)
458
return [d[revnos][1] for revnos in d.keys()]
558
return [revid for (_, revid) in d.itervalues()]
460
def get_branch_nicks(self, changes):
560
def add_branch_nicks(self, change):
462
given a list of changes from L{get_changes}, fill in the branch nicks
463
on all parents and merge points.
562
given a 'change', fill in the branch nicks on all parents and merge
465
565
fetch_set = set()
466
for change in changes:
467
for p in change.parents:
468
fetch_set.add(p.revid)
469
for p in change.merge_points:
470
fetch_set.add(p.revid)
566
for p in change.parents:
567
fetch_set.add(p.revid)
568
for p in change.merge_points:
569
fetch_set.add(p.revid)
471
570
p_changes = self.get_changes(list(fetch_set))
472
571
p_change_dict = dict([(c.revid, c) for c in p_changes])
473
for change in changes:
474
# arch-converted branches may not have merged branch info :(
475
for p in change.parents:
476
if p.revid in p_change_dict:
477
p.branch_nick = p_change_dict[p.revid].branch_nick
479
p.branch_nick = '(missing)'
480
for p in change.merge_points:
481
if p.revid in p_change_dict:
482
p.branch_nick = p_change_dict[p.revid].branch_nick
484
p.branch_nick = '(missing)'
572
for p in change.parents:
573
if p.revid in p_change_dict:
574
p.branch_nick = p_change_dict[p.revid].branch_nick
576
p.branch_nick = '(missing)'
577
for p in change.merge_points:
578
if p.revid in p_change_dict:
579
p.branch_nick = p_change_dict[p.revid].branch_nick
581
p.branch_nick = '(missing)'
486
583
def get_changes(self, revid_list):
487
584
"""Return a list of changes objects for the given revids.
527
624
return [self._change_from_revision(rev) for rev in rev_list]
529
def _get_deltas_for_revisions_with_trees(self, revisions):
530
"""Produce a list of revision deltas.
532
Note that the input is a sequence of REVISIONS, not revision_ids.
533
Trees will be held in memory until the generator exits.
534
Each delta is relative to the revision's lefthand predecessor.
535
(This is copied from bzrlib.)
537
required_trees = set()
538
for revision in revisions:
539
required_trees.add(revision.revid)
540
required_trees.update([p.revid for p in revision.parents[:1]])
541
trees = dict((t.get_revision_id(), t) for
542
t in self._branch.repository.revision_trees(
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
626
def _change_from_revision(self, revision):
557
628
Given a bzrlib Revision, return a processed "change" for use in
560
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
562
parents = [util.Container(revid=r,
563
revno=self.get_revno(r)) for r in revision.parent_ids]
565
631
message, short_message = clean_message(revision.message)
633
tags = self._branch.tags.get_reverse_tag_dict()
636
if tags.has_key(revision.revision_id):
637
revtags = ', '.join(tags[revision.revision_id])
568
640
'revid': revision.revision_id,
570
'author': revision.get_apparent_author(),
641
'date': datetime.datetime.fromtimestamp(revision.timestamp),
642
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
643
'authors': revision.get_apparent_authors(),
571
644
'branch_nick': revision.properties.get('branch-nick', None),
572
645
'short_comment': short_message,
573
646
'comment': revision.message,
574
647
'comment_clean': [util.html_clean(s) for s in message],
575
648
'parents': revision.parent_ids,
649
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
577
652
return util.Container(entry)
579
def get_file_changes_uncached(self, entries):
580
delta_list = self._get_deltas_for_revisions_with_trees(entries)
582
return [self.parse_delta(delta) for delta in delta_list]
584
def get_file_changes(self, entries):
654
def get_file_changes_uncached(self, entry):
656
old_revid = entry.parents[0].revid
658
old_revid = bzrlib.revision.NULL_REVISION
659
return self.file_changes_for_revision_ids(old_revid, entry.revid)
661
def get_file_changes(self, entry):
585
662
if self._file_change_cache is None:
586
return self.get_file_changes_uncached(entries)
663
return self.get_file_changes_uncached(entry)
588
return self._file_change_cache.get_file_changes(entries)
590
def add_changes(self, entries):
591
changes_list = self.get_file_changes(entries)
593
for entry, changes in zip(entries, changes_list):
594
entry.changes = changes
665
return self._file_change_cache.get_file_changes(entry)
667
def add_changes(self, entry):
668
changes = self.get_file_changes(entry)
669
entry.changes = changes
596
671
def get_file(self, file_id, revid):
597
672
"returns (path, filename, data)"
692
text_changes: list((filename, file_id)),
623
for path, fid, kind in delta.added:
624
added.append((rich_filename(path, kind), fid))
626
for path, fid, kind, text_modified, meta_modified in delta.modified:
627
modified.append(util.Container(filename=rich_filename(path, kind),
630
for old_path, new_path, fid, kind, text_modified, meta_modified in \
632
renamed.append((rich_filename(old_path, kind),
633
rich_filename(new_path, kind), fid))
634
if meta_modified or text_modified:
635
modified.append(util.Container(
636
filename=rich_filename(new_path, kind), file_id=fid))
638
for path, fid, kind in delta.removed:
639
removed.append((rich_filename(path, kind), fid))
641
return util.Container(added=added, renamed=renamed,
642
removed=removed, modified=modified)
644
def annotate_file(self, file_id, revid):
649
file_revid = self.get_inventory(revid)[file_id].revision
651
tree = self._branch.repository.revision_tree(file_revid)
655
bzrlib.textfile.check_text_lines(tree.get_file_lines(file_id))
656
except bzrlib.errors.BinaryFile:
657
# bail out; this isn't displayable text
658
yield util.Container(parity=0, lineno=1, status='same',
659
text='(This is a binary file.)',
660
change=util.Container())
694
repo = self._branch.repository
695
if (bzrlib.revision.is_null(old_revid) or
696
bzrlib.revision.is_null(new_revid)):
697
old_tree, new_tree = map(
698
repo.revision_tree, [old_revid, new_revid])
662
for line_revid, text in tree.annotate_iter(file_id):
663
revid_set.add(line_revid)
665
change_cache = dict([(c.revid, c) \
666
for c in self.get_changes(list(revid_set))])
668
last_line_revid = None
669
for line_revid, text in tree.annotate_iter(file_id):
670
if line_revid == last_line_revid:
671
# remember which lines have a new revno and which don't
676
last_line_revid = line_revid
677
change = change_cache[line_revid]
678
trunc_revno = change.revno
679
if len(trunc_revno) > 10:
680
trunc_revno = trunc_revno[:9] + '...'
682
yield util.Container(parity=parity, lineno=lineno, status=status,
683
change=change, text=util.fixed_width(text))
686
self.log.debug('annotate: %r secs' % (time.time() - z))
700
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
702
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
704
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
706
return util.Container(
707
added=sorted(reporter.added, key=lambda x:x.filename),
708
renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
709
removed=sorted(reporter.removed, key=lambda x:x.filename),
710
modified=sorted(reporter.modified, key=lambda x:x.filename),
711
text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))