39
from StringIO import StringIO
41
from loggerhead import search
42
from loggerhead import util
43
from loggerhead.wholehistory import compute_whole_history_data
38
46
import bzrlib.branch
39
47
import bzrlib.delta
40
49
import bzrlib.errors
50
import bzrlib.progress
42
51
import bzrlib.revision
44
from loggerhead import search
45
from loggerhead import util
46
from loggerhead.wholehistory import compute_whole_history_data
52
import bzrlib.textfile
56
# bzrlib's UIFactory is not thread-safe
57
uihack = threading.local()
60
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
62
def nested_progress_bar(self):
63
if getattr(uihack, '_progress_bar_stack', None) is None:
64
pbs = bzrlib.progress.ProgressBarStack(
65
klass=bzrlib.progress.DummyProgress)
66
uihack._progress_bar_stack = pbs
67
return uihack._progress_bar_stack.get_nested()
69
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
49
71
def is_branch(folder):
153
177
file_id=file_id))
156
class RevInfoMemoryCache(object):
157
"""A store that validates values against the revids they were stored with.
159
We use a unique key for each branch.
161
The reason for not just using the revid as the key is so that when a new
162
value is provided for a branch, we replace the old value used for the
165
There is another implementation of the same interface in
166
loggerhead.changecache.RevInfoDiskCache.
169
def __init__(self, cache):
172
def get(self, key, revid):
173
"""Return the data associated with `key`, subject to a revid check.
175
If a value was stored under `key`, with the same revid, return it.
176
Otherwise return None.
178
cached = self._cache.get(key)
181
stored_revid, data = cached
182
if revid == stored_revid:
187
def set(self, key, revid, data):
188
"""Store `data` under `key`, to be checked against `revid` on get().
190
self._cache[key] = (revid, data)
192
# Used to store locks that prevent multiple threads from building a
193
# revision graph for the same branch at the same time, because that can
194
# cause severe performance issues that are so bad that the system seems
196
revision_graph_locks = {}
197
revision_graph_check_lock = threading.Lock()
199
class History(object):
180
class History (object):
200
181
"""Decorate a branch to provide information for rendering.
202
183
History objects are expected to be short lived -- when serving a request
204
185
around it, serve the request, throw the History object away, unlock the
205
186
branch and throw it away.
207
:ivar _file_change_cache: An object that caches information about the
208
files that changed between two revisions.
209
:ivar _rev_info: A list of information about revisions. This is by far
210
the most cryptic data structure in loggerhead. At the top level, it
211
is a list of 3-tuples [(merge-info, where-merged, parents)].
212
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
213
like a merged sorted list, but the revno is stringified.
214
`where-merged` is a tuple of revisions that have this revision as a
215
non-lefthand parent. Finally, `parents` is just the usual list of
216
parents of this revision.
217
:ivar _rev_indices: A dictionary mapping each revision id to the index of
218
the information about it in _rev_info.
219
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
188
:ivar _file_change_cache: xx
223
def _load_whole_history_data(self, caches, cache_key):
224
"""Set the attributes relating to the whole history of the branch.
226
:param caches: a list of caches with interfaces like
227
`RevInfoMemoryCache` and be ordered from fastest to slowest.
228
:param cache_key: the key to use with the caches.
230
self._rev_indices = None
231
self._rev_info = None
234
def update_missed_caches():
235
for cache in missed_caches:
236
cache.set(cache_key, self.last_revid, self._rev_info)
238
# Theoretically, it's possible for two threads to race in creating
239
# the Lock() object for their branch, so we put a lock around
240
# creating the per-branch Lock().
241
revision_graph_check_lock.acquire()
243
if cache_key not in revision_graph_locks:
244
revision_graph_locks[cache_key] = threading.Lock()
246
revision_graph_check_lock.release()
248
revision_graph_locks[cache_key].acquire()
251
data = cache.get(cache_key, self.last_revid)
253
self._rev_info = data
254
update_missed_caches()
257
missed_caches.append(cache)
259
whole_history_data = compute_whole_history_data(self._branch)
260
self._rev_info, self._rev_indices = whole_history_data
261
update_missed_caches()
263
revision_graph_locks[cache_key].release()
265
if self._rev_indices is not None:
266
self._revno_revid = {}
267
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
268
self._revno_revid[revno_str] = revid
270
self._revno_revid = {}
271
self._rev_indices = {}
272
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
273
self._rev_indices[revid] = seq
274
self._revno_revid[revno_str] = revid
276
def __init__(self, branch, whole_history_data_cache, file_cache=None,
277
revinfo_disk_cache=None, cache_key=None):
191
def __init__(self, branch, whole_history_data_cache):
278
192
assert branch.is_locked(), (
279
193
"Can only construct a History object with a read-locked branch.")
280
if file_cache is not None:
281
self._file_change_cache = file_cache
282
file_cache.history = self
284
self._file_change_cache = None
194
self._file_change_cache = None
285
195
self._branch = branch
286
196
self._inventory_cache = {}
287
197
self._branch_nick = self._branch.get_config().get_nickname()
288
self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
198
self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
290
200
self.last_revid = branch.last_revision()
292
caches = [RevInfoMemoryCache(whole_history_data_cache)]
293
if revinfo_disk_cache:
294
caches.append(revinfo_disk_cache)
295
self._load_whole_history_data(caches, cache_key)
202
whole_history_data = whole_history_data_cache.get(self.last_revid)
203
if whole_history_data is None:
204
whole_history_data = compute_whole_history_data(branch)
205
whole_history_data_cache[self.last_revid] = whole_history_data
207
(self._revision_graph, self._full_history, self._revision_info,
208
self._revno_revid, self._merge_sort, self._where_merged,
209
) = whole_history_data
211
def use_file_cache(self, cache):
212
self._file_change_cache = cache
298
215
def has_revisions(self):
315
232
revid in revid_list.
317
234
if revid_list is None:
318
revid_list = [r[0][1] for r in self._rev_info]
235
revid_list = self._full_history
319
236
revid_set = set(revid_list)
320
237
revid = start_revid
322
239
def introduced_revisions(revid):
324
seq = self._rev_indices[revid]
325
md = self._rev_info[seq][0][2]
241
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
327
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
328
r.add(self._rev_info[i][0][1])
243
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
244
r.add(self._merge_sort[i][1])
332
248
if bzrlib.revision.is_null(revid):
334
250
if introduced_revisions(revid) & revid_set:
336
parents = self._rev_info[self._rev_indices[revid]][2]
252
parents = self._revision_graph[revid]
337
253
if len(parents) == 0:
339
255
revid = parents[0]
341
257
def get_short_revision_history_by_fileid(self, file_id):
342
258
# FIXME: would be awesome if we could get, for a folder, the list of
343
259
# revisions where items within that folder changed.i
344
possible_keys = [(file_id, revid) for revid in self._rev_indices]
345
get_parent_map = self._branch.repository.texts.get_parent_map
346
# We chunk the requests as this works better with GraphIndex.
347
# See _filter_revisions_touching_file_id in bzrlib/log.py
348
# for more information.
351
for start in xrange(0, len(possible_keys), chunk_size):
352
next_keys = possible_keys[start:start + chunk_size]
353
revids += [k[1] for k in get_parent_map(next_keys)]
354
del possible_keys, next_keys
261
# FIXME: Workaround for bzr versions prior to 1.6b3.
262
# Remove me eventually pretty please :)
263
w = self._branch.repository.weave_store.get_weave(
264
file_id, self._branch.repository.get_transaction())
265
w_revids = w.versions()
266
revids = [r for r in self._full_history if r in w_revids]
267
except AttributeError:
268
possible_keys = [(file_id, revid) for revid in self._full_history]
269
get_parent_map = self._branch.repository.texts.get_parent_map
270
# We chunk the requests as this works better with GraphIndex.
271
# See _filter_revisions_touching_file_id in bzrlib/log.py
272
# for more information.
275
for start in xrange(0, len(possible_keys), chunk_size):
276
next_keys = possible_keys[start:start + chunk_size]
277
revids += [k[1] for k in get_parent_map(next_keys)]
278
del possible_keys, next_keys
357
281
def get_revision_history_since(self, revid_list, date):
651
575
Given a bzrlib Revision, return a processed "change" for use in
578
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
580
parents = [util.Container(revid=r,
581
revno=self.get_revno(r)) for r in revision.parent_ids]
654
583
message, short_message = clean_message(revision.message)
656
tags = self._branch.tags.get_reverse_tag_dict()
659
if tags.has_key(revision.revision_id):
660
revtags = ', '.join(tags[revision.revision_id])
586
authors = revision.get_apparent_authors()
587
except AttributeError:
588
authors = [revision.get_apparent_author()]
663
591
'revid': revision.revision_id,
664
'date': datetime.datetime.fromtimestamp(revision.timestamp),
665
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
666
'authors': revision.get_apparent_authors(),
667
594
'branch_nick': revision.properties.get('branch-nick', None),
668
595
'short_comment': short_message,
669
596
'comment': revision.message,
670
597
'comment_clean': [util.html_clean(s) for s in message],
671
598
'parents': revision.parent_ids,
672
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
675
if isinstance(revision, bzrlib.foreign.ForeignRevision):
676
foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
677
elif ":" in revision.revision_id:
679
foreign_revid, mapping = \
680
bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
681
revision.revision_id)
682
except bzrlib.errors.InvalidRevisionId:
687
if foreign_revid is not None:
688
entry["foreign_vcs"] = mapping.vcs.abbreviation
689
entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
690
600
return util.Container(entry)
692
602
def get_file_changes_uncached(self, entry):
603
repo = self._branch.repository
693
604
if entry.parents:
694
605
old_revid = entry.parents[0].revid
742
653
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
744
655
return util.Container(
745
added=sorted(reporter.added, key=lambda x: x.filename),
746
renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
747
removed=sorted(reporter.removed, key=lambda x: x.filename),
748
modified=sorted(reporter.modified, key=lambda x: x.filename),
749
text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))
656
added=sorted(reporter.added, key=lambda x:x.filename),
657
renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
658
removed=sorted(reporter.removed, key=lambda x:x.filename),
659
modified=sorted(reporter.modified, key=lambda x:x.filename),
660
text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))