52
60
with_branch_lock = util.with_lock('_lock', 'branch')
56
def with_bzrlib_read_lock(unbound):
57
def bzrlib_read_locked(self, *args, **kw):
58
#self.log.debug('-> %r bzr lock', id(threading.currentThread()))
59
self._branch.repository.lock_read()
61
return unbound(self, *args, **kw)
63
self._branch.repository.unlock()
64
#self.log.debug('<- %r bzr lock', id(threading.currentThread()))
65
return bzrlib_read_locked
68
63
# bzrlib's UIFactory is not thread-safe
69
64
uihack = threading.local()
191
186
class History (object):
193
188
def __init__(self):
189
self._change_cache = None
194
190
self._file_change_cache = None
195
192
self._lock = threading.RLock()
198
def from_branch(cls, branch):
195
def from_branch(cls, branch, name=None):
201
198
self._branch = branch
202
199
self._last_revid = self._branch.last_revision()
204
self.log = logging.getLogger('loggerhead.%s' % (self._branch.nick,))
206
graph = branch.repository.get_graph()
207
parent_map = dict(((key, value) for key, value in
208
graph.iter_ancestry([self._last_revid]) if value is not None))
210
self._revision_graph = self._strip_NULL_ghosts(parent_map)
200
if self._last_revid is not None:
201
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
203
self._revision_graph = {}
206
name = self._branch.nick
208
self.log = logging.getLogger('loggerhead.%s' % (name,))
211
210
self._full_history = []
212
211
self._revision_info = {}
213
212
self._revno_revid = {}
214
if bzrlib.revision.is_null(self._last_revid):
215
self._merge_sort = []
217
self._merge_sort = bzrlib.tsort.merge_sort(
218
self._revision_graph, self._last_revid, generate_revno=True)
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
220
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
221
215
self._full_history.append(revid)
222
216
revno_str = '.'.join(str(n) for n in revno)
223
217
self._revno_revid[revno_str] = revid
224
self._revision_info[revid] = (
225
seq, revid, merge_depth, revno_str, end_of_merge)
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
227
220
# cache merge info
228
221
self._where_merged = {}
230
222
for revid in self._revision_graph.keys():
231
if self._revision_info[revid][2] == 0:
223
if not revid in self._full_history:
233
225
for parent in self._revision_graph[revid]:
234
226
self._where_merged.setdefault(parent, set()).add(revid)
236
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
240
def _strip_NULL_ghosts(revision_graph):
242
Copied over from bzrlib meant as a temporary workaround deprecated
246
# Filter ghosts, and null:
247
if bzrlib.revision.NULL_REVISION in revision_graph:
248
del revision_graph[bzrlib.revision.NULL_REVISION]
249
for key, parents in revision_graph.items():
250
revision_graph[key] = tuple(parent for parent in parents if parent
252
return revision_graph
255
def from_folder(cls, path):
232
def from_folder(cls, path, name=None):
256
233
b = bzrlib.branch.Branch.open(path)
259
return cls.from_branch(b)
234
return cls.from_branch(b, name)
263
236
@with_branch_lock
264
237
def out_of_date(self):
265
238
# the branch may have been upgraded on disk, in which case we're stale.
266
newly_opened = bzrlib.branch.Branch.open(self._branch.base)
267
239
if self._branch.__class__ is not \
268
newly_opened.__class__:
270
if self._branch.repository.__class__ is not \
271
newly_opened.repository.__class__:
240
bzrlib.branch.Branch.open(self._branch.base).__class__:
273
242
return self._branch.last_revision() != self._last_revid
244
def use_cache(self, cache):
245
self._change_cache = cache
275
247
def use_file_cache(self, cache):
276
248
self._file_change_cache = cache
279
def has_revisions(self):
280
return not bzrlib.revision.is_null(self.last_revid)
250
def use_search_index(self, index):
255
# called when a new history object needs to be created, because the
256
# branch history has changed. we need to immediately close and stop
257
# using our caches, because a new history object will be created to
258
# replace us, using the same cache files.
259
# (may also be called during server shutdown.)
260
if self._change_cache is not None:
261
self._change_cache.close()
262
self._change_cache = None
263
if self._index is not None:
267
def flush_cache(self):
268
if self._change_cache is None:
270
self._change_cache.flush()
272
def check_rebuild(self):
273
if self._change_cache is not None:
274
self._change_cache.check_rebuild()
275
if self._index is not None:
276
self._index.check_rebuild()
282
278
last_revid = property(lambda self: self._last_revid, None, None)
295
291
def get_revision_history(self):
296
292
return self._full_history
298
def get_revids_from(self, revid_list, start_revid):
300
Yield the mainline (wrt start_revid) revisions that merged each
303
if revid_list is None:
304
revid_list = self._full_history
305
revid_set = set(revid_list)
307
def introduced_revisions(revid):
309
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
311
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
312
r.add(self._merge_sort[i][1])
316
if bzrlib.revision.is_null(revid):
318
if introduced_revisions(revid) & revid_set:
294
def get_revids_from(self, revid_list, revid):
296
given a list of revision ids, yield revisions in graph order,
297
starting from revid. the list can be None if you just want to travel
298
across all revisions.
301
if (revid_list is None) or (revid in revid_list):
303
if not self._revision_graph.has_key(revid):
320
305
parents = self._revision_graph[revid]
321
306
if len(parents) == 0:
348
333
return revid_list[index:]
350
335
@with_branch_lock
336
def get_revision_history_matching(self, revid_list, text):
337
self.log.debug('searching %d revisions for %r', len(revid_list), text)
339
# this is going to be painfully slow. :(
342
for revid in revid_list:
343
change = self.get_changes([ revid ])[0]
344
if text in change.comment.lower():
346
self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
349
def get_revision_history_matching_indexed(self, revid_list, text):
350
self.log.debug('searching %d revisions for %r', len(revid_list), text)
352
if self._index is None:
353
return self.get_revision_history_matching(revid_list, text)
354
out = self._index.find(text, revid_list)
355
self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
356
# put them in some coherent order :)
357
out = [r for r in self._full_history if r in out]
351
361
def get_search_revid_list(self, query, revid_list):
353
363
given a "quick-search" query, try a few obvious possible meanings:
389
399
revid_list = list(self.get_revids_from(None, self._last_revid))
390
400
return self.get_revision_history_since(revid_list, date)
402
# check comment fields.
403
if revid_list is None:
404
revid_list = self._full_history
405
return self.get_revision_history_matching_indexed(revid_list, query)
392
407
revno_re = re.compile(r'^[\d\.]+$')
393
408
# the date regex are without a final '$' so that queries like
394
409
# "2006-11-30 12:15" still mostly work. (i think it's better to give
434
449
determine the revision list we're viewing (start_revid, file_id, query)
435
450
and where we are in it (revid).
437
- if a query is given, we're viewing query results.
438
- if a file_id is given, we're viewing revisions for a specific
440
- if a start_revid is given, we're viewing the branch from a
441
specific revision up the tree.
443
these may be combined to view revisions for a specific file, from
444
a specific revision, with a specific search query.
446
returns a new (revid, start_revid, revid_list) where:
452
if a query is given, we're viewing query results.
453
if a file_id is given, we're viewing revisions for a specific file.
454
if a start_revid is given, we're viewing the branch from a
455
specific revision up the tree.
456
(these may be combined to view revisions for a specific file, from
457
a specific revision, with a specific search query.)
459
returns a new (revid, start_revid, revid_list, scan_list) where:
448
461
- revid: current position within the view
449
462
- start_revid: starting revision of this view
462
475
if revid not in revid_list:
463
476
# if the given revid is not in the revlist, use a revlist that
464
477
# starts at the given revid.
465
revid_list = self.get_file_view(revid, file_id)
478
revid_list= self.get_file_view(revid, file_id)
466
479
start_revid = revid
467
480
return revid, start_revid, revid_list
575
590
@with_branch_lock
576
591
def get_changes(self, revid_list):
577
"""Return a list of changes objects for the given revids.
579
Revisions not present and NULL_REVISION will be ignored.
581
changes = self.get_changes_uncached(revid_list)
592
if self._change_cache is None:
593
changes = self.get_changes_uncached(revid_list)
595
changes = self._change_cache.get_changes(revid_list)
582
596
if len(changes) == 0:
603
@with_bzrlib_read_lock
604
def get_changes_uncached(self, revid_list):
605
# FIXME: deprecated method in getting a null revision
606
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
608
parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
609
# We need to return the answer in the same order as the input,
611
present_revids = [revid for revid in revid_list
612
if revid in parent_map]
613
rev_list = self._branch.repository.get_revisions(present_revids)
615
return [self._change_from_revision(rev) for rev in rev_list]
617
def _get_deltas_for_revisions_with_trees(self, revisions):
618
"""Produce a list of revision deltas.
613
# alright, let's profile this sucka.
614
def _get_changes_profiled(self, revid_list, get_diffs=False):
615
from loggerhead.lsprof import profile
617
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
620
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
621
self.log.info('lsprof complete!')
624
def _get_deltas_for_revisions_with_trees(self, entries):
625
"""Produce a generator of revision deltas.
620
627
Note that the input is a sequence of REVISIONS, not revision_ids.
621
628
Trees will be held in memory until the generator exits.
622
629
Each delta is relative to the revision's lefthand predecessor.
623
(This is copied from bzrlib.)
625
631
required_trees = set()
626
for revision in revisions:
627
required_trees.add(revision.revid)
628
required_trees.update([p.revid for p in revision.parents[:1]])
632
for entry in entries:
633
required_trees.add(entry.revid)
634
required_trees.update([p.revid for p in entry.parents[:1]])
629
635
trees = dict((t.get_revision_id(), t) for
630
636
t in self._branch.repository.revision_trees(required_trees))
632
638
self._branch.repository.lock_read()
634
for revision in revisions:
635
if not revision.parents:
640
for entry in entries:
641
if not entry.parents:
636
642
old_tree = self._branch.repository.revision_tree(
637
643
bzrlib.revision.NULL_REVISION)
639
old_tree = trees[revision.parents[0].revid]
640
tree = trees[revision.revid]
645
old_tree = trees[entry.parents[0].revid]
646
tree = trees[entry.revid]
641
647
ret.append(tree.changes_from(old_tree))
644
650
self._branch.repository.unlock()
646
def _change_from_revision(self, revision):
648
Given a bzrlib Revision, return a processed "change" for use in
652
def entry_from_revision(self, revision):
651
653
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
653
655
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
662
664
'short_comment': short_message,
663
665
'comment': revision.message,
664
666
'comment_clean': [util.html_clean(s) for s in message],
665
'parents': revision.parent_ids,
667
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()
669
692
def get_file_changes_uncached(self, entries):
670
693
delta_list = self._get_deltas_for_revisions_with_trees(entries)
698
721
rev_tree2 = self._branch.repository.revision_tree(revid)
699
722
delta = rev_tree2.changes_from(rev_tree1)
701
change.changes = self.parse_delta(delta)
702
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
724
entry.changes = self.parse_delta(delta)
726
entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
706
730
@with_branch_lock
707
731
def get_file(self, file_id, revid):
895
919
file_revid = self.get_inventory(revid)[file_id].revision
897
tree = self._branch.repository.revision_tree(file_revid)
922
# because we cache revision metadata ourselves, it's actually much
923
# faster to call 'annotate_iter' on the weave directly than it is to
924
# ask bzrlib to annotate for us.
925
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
898
927
revid_set = set()
900
for line_revid, text in tree.annotate_iter(file_id):
928
for line_revid, text in w.annotate_iter(file_revid):
901
929
revid_set.add(line_revid)
902
930
if self._BADCHARS_RE.match(text):
903
931
# bail out; this isn't displayable text
905
933
text='(This is a binary file.)',
906
934
change=util.Container())
908
change_cache = dict([(c.revid, c) \
909
for c in self.get_changes(list(revid_set))])
936
change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
911
938
last_line_revid = None
912
for line_revid, text in tree.annotate_iter(file_id):
939
for line_revid, text in w.annotate_iter(file_revid):
913
940
if line_revid == last_line_revid:
914
941
# remember which lines have a new revno and which don't