95
100
def __getitem__(self, index):
96
101
"""Get the date of the index'd item"""
97
return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
102
return datetime.datetime.fromtimestamp(self.repository.get_revision(
103
self.revid_list[index]).timestamp)
99
105
def __len__(self):
100
106
return len(self.revid_list)
103
class History (object):
106
self._change_cache = None
108
self._lock = threading.RLock()
111
def from_branch(cls, branch, name=None):
108
class FileChangeReporter(object):
110
def __init__(self, old_inv, new_inv):
115
self.text_changes = []
116
self.old_inv = old_inv
117
self.new_inv = new_inv
119
def revid(self, inv, file_id):
121
return inv[file_id].revision
122
except bzrlib.errors.NoSuchId:
125
def report(self, file_id, paths, versioned, renamed, modified,
127
if modified not in ('unchanged', 'kind changed'):
128
if versioned == 'removed':
129
filename = rich_filename(paths[0], kind[0])
131
filename = rich_filename(paths[1], kind[1])
132
self.text_changes.append(util.Container(
133
filename=filename, file_id=file_id,
134
old_revision=self.revid(self.old_inv, file_id),
135
new_revision=self.revid(self.new_inv, file_id)))
136
if versioned == 'added':
137
self.added.append(util.Container(
138
filename=rich_filename(paths[1], kind),
139
file_id=file_id, kind=kind[1]))
140
elif versioned == 'removed':
141
self.removed.append(util.Container(
142
filename=rich_filename(paths[0], kind),
143
file_id=file_id, kind=kind[0]))
145
self.renamed.append(util.Container(
146
old_filename=rich_filename(paths[0], kind[0]),
147
new_filename=rich_filename(paths[1], kind[1]),
149
text_modified=modified == 'modified'))
151
self.modified.append(util.Container(
152
filename=rich_filename(paths[1], kind),
155
# The lru_cache is not thread-safe, so we need a lock around it for
157
rev_info_memory_cache_lock = threading.RLock()
159
class RevInfoMemoryCache(object):
160
"""A store that validates values against the revids they were stored with.
162
We use a unique key for each branch.
164
The reason for not just using the revid as the key is so that when a new
165
value is provided for a branch, we replace the old value used for the
168
There is another implementation of the same interface in
169
loggerhead.changecache.RevInfoDiskCache.
172
def __init__(self, cache):
175
def get(self, key, revid):
176
"""Return the data associated with `key`, subject to a revid check.
178
If a value was stored under `key`, with the same revid, return it.
179
Otherwise return None.
181
rev_info_memory_cache_lock.acquire()
183
cached = self._cache.get(key)
185
rev_info_memory_cache_lock.release()
188
stored_revid, data = cached
189
if revid == stored_revid:
194
def set(self, key, revid, data):
195
"""Store `data` under `key`, to be checked against `revid` on get().
197
rev_info_memory_cache_lock.acquire()
199
self._cache[key] = (revid, data)
201
rev_info_memory_cache_lock.release()
203
# Used to store locks that prevent multiple threads from building a
204
# revision graph for the same branch at the same time, because that can
205
# cause severe performance issues that are so bad that the system seems
207
revision_graph_locks = {}
208
revision_graph_check_lock = threading.Lock()
210
class History(object):
211
"""Decorate a branch to provide information for rendering.
213
History objects are expected to be short lived -- when serving a request
214
for a particular branch, open it, read-lock it, wrap a History object
215
around it, serve the request, throw the History object away, unlock the
216
branch and throw it away.
218
:ivar _file_change_cache: An object that caches information about the
219
files that changed between two revisions.
220
:ivar _rev_info: A list of information about revisions. This is by far
221
the most cryptic data structure in loggerhead. At the top level, it
222
is a list of 3-tuples [(merge-info, where-merged, parents)].
223
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
224
like a merged sorted list, but the revno is stringified.
225
`where-merged` is a tuple of revisions that have this revision as a
226
non-lefthand parent. Finally, `parents` is just the usual list of
227
parents of this revision.
228
:ivar _rev_indices: A dictionary mapping each revision id to the index of
229
the information about it in _rev_info.
230
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
234
def _load_whole_history_data(self, caches, cache_key):
235
"""Set the attributes relating to the whole history of the branch.
237
:param caches: a list of caches with interfaces like
238
`RevInfoMemoryCache` and be ordered from fastest to slowest.
239
:param cache_key: the key to use with the caches.
241
self._rev_indices = None
242
self._rev_info = None
245
def update_missed_caches():
246
for cache in missed_caches:
247
cache.set(cache_key, self.last_revid, self._rev_info)
249
# Theoretically, it's possible for two threads to race in creating
250
# the Lock() object for their branch, so we put a lock around
251
# creating the per-branch Lock().
252
revision_graph_check_lock.acquire()
254
if cache_key not in revision_graph_locks:
255
revision_graph_locks[cache_key] = threading.Lock()
257
revision_graph_check_lock.release()
259
revision_graph_locks[cache_key].acquire()
262
data = cache.get(cache_key, self.last_revid)
264
self._rev_info = data
265
update_missed_caches()
268
missed_caches.append(cache)
270
whole_history_data = compute_whole_history_data(self._branch)
271
self._rev_info, self._rev_indices = whole_history_data
272
update_missed_caches()
274
revision_graph_locks[cache_key].release()
276
if self._rev_indices is not None:
277
self._revno_revid = {}
278
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
279
self._revno_revid[revno_str] = revid
281
self._revno_revid = {}
282
self._rev_indices = {}
283
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
284
self._rev_indices[revid] = seq
285
self._revno_revid[revno_str] = revid
287
def __init__(self, branch, whole_history_data_cache, file_cache=None,
288
revinfo_disk_cache=None, cache_key=None):
289
assert branch.is_locked(), (
290
"Can only construct a History object with a read-locked branch.")
291
if file_cache is not None:
292
self._file_change_cache = file_cache
293
file_cache.history = self
295
self._file_change_cache = None
114
296
self._branch = branch
115
self._history = branch.revision_history()
116
self._last_revid = self._history[-1]
117
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
120
name = self._branch.nick
122
self.log = logging.getLogger('loggerhead.%s' % (name,))
124
self._full_history = []
125
self._revision_info = {}
126
self._revno_revid = {}
127
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
129
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
130
self._full_history.append(revid)
131
revno_str = '.'.join(str(n) for n in revno)
132
self._revno_revid[revno_str] = revid
133
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
138
self._where_merged = {}
139
for revid in self._revision_graph.keys():
140
if not revid in self._full_history:
142
for parent in self._revision_graph[revid]:
143
self._where_merged.setdefault(parent, set()).add(revid)
145
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
149
def from_folder(cls, path, name=None):
150
b = bzrlib.branch.Branch.open(path)
151
return cls.from_branch(b, name)
154
def out_of_date(self):
155
if self._branch.revision_history()[-1] != self._last_revid:
159
def use_cache(self, cache):
160
self._change_cache = cache
162
def use_search_index(self, index):
167
# called when a new history object needs to be created, because the
168
# branch history has changed. we need to immediately close and stop
169
# using our caches, because a new history object will be created to
170
# replace us, using the same cache files.
171
if self._change_cache is not None:
172
self._change_cache.close()
173
self._change_cache = None
174
if self._index is not None:
178
def flush_cache(self):
179
if self._change_cache is None:
181
self._change_cache.flush()
183
def check_rebuild(self):
184
if self._change_cache is not None:
185
self._change_cache.check_rebuild()
186
if self._index is not None:
187
self._index.check_rebuild()
189
last_revid = property(lambda self: self._last_revid, None, None)
191
count = property(lambda self: self._count, None, None)
194
def get_revision(self, revid):
195
return self._branch.repository.get_revision(revid)
297
self._branch_tags = None
298
self._inventory_cache = {}
299
self._branch_nick = self._branch.get_config().get_nickname()
300
self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
302
self.last_revid = branch.last_revision()
304
caches = [RevInfoMemoryCache(whole_history_data_cache)]
305
if revinfo_disk_cache:
306
caches.append(revinfo_disk_cache)
307
self._load_whole_history_data(caches, cache_key)
310
def has_revisions(self):
311
return not bzrlib.revision.is_null(self.last_revid)
313
def get_config(self):
314
return self._branch.get_config()
197
316
def get_revno(self, revid):
198
if revid not in self._revision_info:
317
if revid not in self._rev_indices:
201
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
320
seq = self._rev_indices[revid]
321
revno = self._rev_info[seq][0][3]
204
def get_sequence(self, revid):
205
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
208
def get_revision_history(self):
209
return self._full_history
211
def get_revid_sequence(self, revid_list, revid):
213
given a list of revision ids, return the sequence # of this revid in
222
def get_revids_from(self, revid_list, revid):
224
given a list of revision ids, yield revisions in graph order,
225
starting from revid. the list can be None if you just want to travel
226
across all revisions.
229
if (revid_list is None) or (revid in revid_list):
324
def get_revids_from(self, revid_list, start_revid):
326
Yield the mainline (wrt start_revid) revisions that merged each
329
if revid_list is None:
330
# Just yield the mainline, starting at start_revid
332
is_null = bzrlib.revision.is_null
333
while not is_null(revid):
231
if not self._revision_graph.has_key(revid):
335
parents = self._rev_info[self._rev_indices[revid]][2]
340
revid_set = set(revid_list)
343
def introduced_revisions(revid):
345
seq = self._rev_indices[revid]
346
md = self._rev_info[seq][0][2]
348
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
349
r.add(self._rev_info[i][0][1])
353
if bzrlib.revision.is_null(revid):
233
parents = self._revision_graph[revid]
355
rev_introduced = introduced_revisions(revid)
356
matching = rev_introduced.intersection(revid_set)
358
# We don't need to look for these anymore.
359
revid_set.difference_update(matching)
361
parents = self._rev_info[self._rev_indices[revid]][2]
234
362
if len(parents) == 0:
236
364
revid = parents[0]
239
366
def get_short_revision_history_by_fileid(self, file_id):
240
# wow. is this really the only way we can get this list? by
241
# man-handling the weave store directly? :-0
242
367
# FIXME: would be awesome if we could get, for a folder, the list of
243
# revisions where items within that folder changed.
244
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
245
w_revids = w.versions()
246
revids = [r for r in self._full_history if r in w_revids]
368
# revisions where items within that folder changed.i
369
possible_keys = [(file_id, revid) for revid in self._rev_indices]
370
get_parent_map = self._branch.repository.texts.get_parent_map
371
# We chunk the requests as this works better with GraphIndex.
372
# See _filter_revisions_touching_file_id in bzrlib/log.py
373
# for more information.
376
for start in xrange(0, len(possible_keys), chunk_size):
377
next_keys = possible_keys[start:start + chunk_size]
378
revids += [k[1] for k in get_parent_map(next_keys)]
379
del possible_keys, next_keys
250
382
def get_revision_history_since(self, revid_list, date):
251
383
# if a user asks for revisions starting at 01-sep, they mean inclusive,
252
384
# so start at midnight on 02-sep.
253
385
date = date + datetime.timedelta(days=1)
254
# our revid list is sorted in REVERSE date order, so go thru some hoops here...
386
# our revid list is sorted in REVERSE date order,
387
# so go thru some hoops here...
255
388
revid_list.reverse()
256
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
389
index = bisect.bisect(_RevListToTimestamps(revid_list,
390
self._branch.repository),
259
394
revid_list.reverse()
261
396
return revid_list[index:]
264
def get_revision_history_matching(self, revid_list, text):
265
self.log.debug('searching %d revisions for %r', len(revid_list), text)
267
# this is going to be painfully slow. :(
270
for revid in revid_list:
271
change = self.get_changes([ revid ])[0]
272
if text in change.comment.lower():
274
self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
277
def get_revision_history_matching_indexed(self, revid_list, text):
278
self.log.debug('searching %d revisions for %r', len(revid_list), text)
280
if self._index is None:
281
return self.get_revision_history_matching(revid_list, text)
282
out = self._index.find(text, revid_list)
283
self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
284
# put them in some coherent order :)
285
out = [r for r in self._full_history if r in out]
289
398
def get_search_revid_list(self, query, revid_list):
291
400
given a "quick-search" query, try a few obvious possible meanings:
293
402
- revision id or # ("128.1.3")
294
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
403
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
404
iso style "yyyy-mm-dd")
295
405
- comment text as a fallback
297
407
and return a revid list that matches.
342
457
# if a "revid" is actually a dotted revno, convert it to a revid
343
458
if revid is None:
345
if self.revno_re.match(revid):
346
revid = self._revno_revid[revid]
461
return self.last_revid
463
if self.revno_re.match(revid):
464
revid = self._revno_revid[revid]
466
raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
350
469
def get_file_view(self, revid, file_id):
352
Given an optional revid and optional path, return a (revlist, revid)
353
for navigation through the current scope: from the revid (or the
354
latest revision) back to the original revision.
471
Given a revid and optional path, return a (revlist, revid) for
472
navigation through the current scope: from the revid (or the latest
473
revision) back to the original revision.
356
475
If file_id is None, the entire revision history is the list scope.
357
If revid is None, the latest revision is used.
359
477
if revid is None:
360
revid = self._last_revid
478
revid = self.last_revid
361
479
if file_id is not None:
362
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
363
inv = self._branch.repository.get_revision_inventory(revid)
364
revlist = list(self.get_short_revision_history_by_fileid(file_id))
365
revlist = list(self.get_revids_from(revlist, revid))
481
self.get_short_revision_history_by_fileid(file_id))
482
revlist = self.get_revids_from(revlist, revid)
367
revlist = list(self.get_revids_from(None, revid))
370
return revlist, revid
373
def get_view(self, revid, start_revid, file_id, query=None):
484
revlist = self.get_revids_from(None, revid)
488
def _iterate_sufficiently(iterable, stop_at, extra_rev_count):
489
"""Return a list of iterable.
491
If extra_rev_count is None, fully consume iterable.
492
Otherwise, stop at 'stop_at' + extra_rev_count.
495
iterate until you find stop_at, then iterate 10 more times.
497
if extra_rev_count is None:
498
return list(iterable)
507
for count, n in enumerate(iterable):
508
if count >= extra_rev_count:
513
def get_view(self, revid, start_revid, file_id, query=None,
514
extra_rev_count=None):
375
516
use the URL parameters (revid, start_revid, file_id, and query) to
376
517
determine the revision list we're viewing (start_revid, file_id, query)
377
518
and where we are in it (revid).
379
if a query is given, we're viewing query results.
380
if a file_id is given, we're viewing revisions for a specific file.
381
if a start_revid is given, we're viewing the branch from a
382
specific revision up the tree.
383
(these may be combined to view revisions for a specific file, from
384
a specific revision, with a specific search query.)
386
returns a new (revid, start_revid, revid_list, scan_list) where:
520
- if a query is given, we're viewing query results.
521
- if a file_id is given, we're viewing revisions for a specific
523
- if a start_revid is given, we're viewing the branch from a
524
specific revision up the tree.
525
- if extra_rev_count is given, find the view from start_revid =>
526
revid, and continue an additional 'extra_rev_count'. If not
527
given, then revid_list will contain the full history of
530
these may be combined to view revisions for a specific file, from
531
a specific revision, with a specific search query.
533
returns a new (revid, start_revid, revid_list) where:
388
535
- revid: current position within the view
389
536
- start_revid: starting revision of this view
390
537
- revid_list: list of revision ids for this view
392
539
file_id and query are never changed so aren't returned, but they may
393
540
contain vital context for future url navigation.
542
if start_revid is None:
543
start_revid = self.last_revid
395
545
if query is None:
396
revid_list, start_revid = self.get_file_view(start_revid, file_id)
546
revid_list = self.get_file_view(start_revid, file_id)
547
revid_list = self._iterate_sufficiently(revid_list, revid,
397
549
if revid is None:
398
550
revid = start_revid
399
551
if revid not in revid_list:
400
552
# if the given revid is not in the revlist, use a revlist that
401
553
# starts at the given revid.
402
revid_list, start_revid = self.get_file_view(revid, file_id)
554
revid_list = self.get_file_view(revid, file_id)
555
revid_list = self._iterate_sufficiently(revid_list, revid,
403
558
return revid, start_revid, revid_list
405
560
# potentially limit the search
406
if (start_revid is not None) or (file_id is not None):
407
revid_list, start_revid = self.get_file_view(start_revid, file_id)
561
if file_id is not None:
562
revid_list = self.get_file_view(start_revid, file_id)
409
564
revid_list = None
411
revid_list = self.get_search_revid_list(query, revid_list)
412
if len(revid_list) > 0:
565
revid_list = search.search_revisions(self._branch, query)
566
if revid_list and len(revid_list) > 0:
413
567
if revid not in revid_list:
414
568
revid = revid_list[0]
415
569
return revid, start_revid, revid_list
571
# XXX: This should return a message saying that the search could
572
# not be completed due to either missing the plugin or missing a
418
574
return None, None, []
421
576
def get_inventory(self, revid):
422
return self._branch.repository.get_revision_inventory(revid)
577
if revid not in self._inventory_cache:
578
self._inventory_cache[revid] = (
579
self._branch.repository.get_inventory(revid))
580
return self._inventory_cache[revid]
425
582
def get_path(self, revid, file_id):
426
583
if (file_id is None) or (file_id == ''):
428
path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
585
path = self.get_inventory(revid).id2path(file_id)
429
586
if (len(path) > 0) and not path.startswith('/'):
430
587
path = '/' + path
433
def get_where_merged(self, revid):
435
return self._where_merged[revid]
590
def get_file_id(self, revid, path):
591
if (len(path) > 0) and not path.startswith('/'):
593
return self.get_inventory(revid).path2id(path)
439
595
def get_merge_point_list(self, revid):
441
597
Return the list of revids that have merged this node.
443
if revid in self._history:
599
if '.' not in self.get_revno(revid):
448
children = self.get_where_merged(revid)
604
children = self._rev_info[self._rev_indices[revid]][1]
450
606
for child in children:
451
child_parents = self._revision_graph[child]
607
child_parents = self._rev_info[self._rev_indices[child]][2]
452
608
if child_parents[0] == revid:
453
609
nexts.append(child)
474
630
revnol = revno.split(".")
475
631
revnos = ".".join(revnol[:-2])
476
632
revnolast = int(revnol[-1])
477
if d.has_key(revnos):
479
635
if revnolast < m:
480
d[revnos] = ( revnolast, revid )
636
d[revnos] = (revnolast, revid)
482
d[revnos] = ( revnolast, revid )
484
return [ d[revnos][1] for revnos in d.keys() ]
486
def get_branch_nicks(self, changes):
638
d[revnos] = (revnolast, revid)
640
return [revid for (_, revid) in d.itervalues()]
642
def add_branch_nicks(self, change):
488
given a list of changes from L{get_changes}, fill in the branch nicks
489
on all parents and merge points.
644
given a 'change', fill in the branch nicks on all parents and merge
491
647
fetch_set = set()
492
for change in changes:
493
for p in change.parents:
494
fetch_set.add(p.revid)
495
for p in change.merge_points:
496
fetch_set.add(p.revid)
648
for p in change.parents:
649
fetch_set.add(p.revid)
650
for p in change.merge_points:
651
fetch_set.add(p.revid)
497
652
p_changes = self.get_changes(list(fetch_set))
498
653
p_change_dict = dict([(c.revid, c) for c in p_changes])
499
for change in changes:
500
for p in change.parents:
501
p.branch_nick = p_change_dict[p.revid].branch_nick
502
for p in change.merge_points:
503
p.branch_nick = p_change_dict[p.revid].branch_nick
506
def get_changes(self, revid_list, get_diffs=False):
507
if self._change_cache is None:
508
changes = self.get_changes_uncached(revid_list, get_diffs)
510
changes = self._change_cache.get_changes(revid_list, get_diffs)
654
for p in change.parents:
655
if p.revid in p_change_dict:
656
p.branch_nick = p_change_dict[p.revid].branch_nick
658
p.branch_nick = '(missing)'
659
for p in change.merge_points:
660
if p.revid in p_change_dict:
661
p.branch_nick = p_change_dict[p.revid].branch_nick
663
p.branch_nick = '(missing)'
665
def get_changes(self, revid_list):
666
"""Return a list of changes objects for the given revids.
668
Revisions not present and NULL_REVISION will be ignored.
670
changes = self.get_changes_uncached(revid_list)
671
if len(changes) == 0:
514
674
# some data needs to be recalculated each time, because it may
515
675
# change as new revisions are added.
516
for i in xrange(len(revid_list)):
517
revid = revid_list[i]
519
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
520
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
676
for change in changes:
677
merge_revids = self.simplify_merge_point_list(
678
self.get_merge_point_list(change.revid))
679
change.merge_points = [
680
util.Container(revid=r,
681
revno=self.get_revno(r)) for r in merge_revids]
682
if len(change.parents) > 0:
683
change.parents = [util.Container(revid=r,
684
revno=self.get_revno(r)) for r in change.parents]
685
change.revno = self.get_revno(change.revid)
688
for change in changes:
689
change.parity = parity
524
# alright, let's profile this sucka.
525
def _get_changes_profiled(self, revid_list, get_diffs=False):
526
from loggerhead.lsprof import profile
528
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
531
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
535
@with_bzrlib_read_lock
536
def get_changes_uncached(self, revid_list, get_diffs=False):
538
rev_list = self._branch.repository.get_revisions(revid_list)
539
except (KeyError, bzrlib.errors.NoSuchRevision):
542
delta_list = self._branch.repository.get_deltas_for_revisions(rev_list)
543
combined_list = zip(rev_list, delta_list)
547
# lookup the trees for each revision, so we can calculate diffs
550
lookup_set.add(rev.revision_id)
551
if len(rev.parent_ids) > 0:
552
lookup_set.add(rev.parent_ids[0])
553
tree_map = dict((t.get_revision_id(), t) for t in self._branch.repository.revision_trees(lookup_set))
554
# also the root tree, in case we hit the origin:
555
tree_map[None] = self._branch.repository.revision_tree(None)
558
for rev, delta in combined_list:
559
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
561
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
563
if len(parents) == 0:
566
left_parent = rev.parent_ids[0]
568
message = rev.message.splitlines()
569
if len(message) == 1:
570
# robey-style 1-line long message
571
message = textwrap.wrap(message[0])
573
# make short form of commit message
574
short_message = message[0]
575
if len(short_message) > 60:
576
short_message = short_message[:60] + '...'
578
old_tree, new_tree = None, None
580
new_tree = tree_map[rev.revision_id]
581
old_tree = tree_map[left_parent]
584
'revid': rev.revision_id,
585
'revno': self.get_revno(rev.revision_id),
587
'author': rev.committer,
588
'branch_nick': rev.properties.get('branch-nick', None),
589
'short_comment': short_message,
590
'comment': rev.message,
591
'comment_clean': [util.html_clean(s) for s in message],
593
'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
595
entries.append(util.Container(entry))
694
def get_changes_uncached(self, revid_list):
695
# FIXME: deprecated method in getting a null revision
696
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
698
parent_map = self._branch.repository.get_graph().get_parent_map(
700
# We need to return the answer in the same order as the input,
702
present_revids = [revid for revid in revid_list
703
if revid in parent_map]
704
rev_list = self._branch.repository.get_revisions(present_revids)
706
return [self._change_from_revision(rev) for rev in rev_list]
708
def _change_from_revision(self, revision):
710
Given a bzrlib Revision, return a processed "change" for use in
713
message, short_message = clean_message(revision.message)
715
if self._branch_tags is None:
716
self._branch_tags = self._branch.tags.get_reverse_tag_dict()
719
if revision.revision_id in self._branch_tags:
720
# tag.sort_* functions expect (tag, data) pairs, so we generate them,
721
# and then strip them
722
tags = [(t, None) for t in self._branch_tags[revision.revision_id]]
723
sort_func = getattr(tag, 'sort_natural', None)
724
if sort_func is None:
727
sort_func(self._branch, tags)
728
revtags = u', '.join([t[0] for t in tags])
731
'revid': revision.revision_id,
732
'date': datetime.datetime.fromtimestamp(revision.timestamp),
733
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
734
'committer': revision.committer,
735
'authors': revision.get_apparent_authors(),
736
'branch_nick': revision.properties.get('branch-nick', None),
737
'short_comment': short_message,
738
'comment': revision.message,
739
'comment_clean': [util.html_clean(s) for s in message],
740
'parents': revision.parent_ids,
741
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
744
if isinstance(revision, bzrlib.foreign.ForeignRevision):
745
foreign_revid, mapping = (
746
revision.foreign_revid, revision.mapping)
747
elif ":" in revision.revision_id:
749
foreign_revid, mapping = \
750
bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
751
revision.revision_id)
752
except bzrlib.errors.InvalidRevisionId:
757
if foreign_revid is not None:
758
entry["foreign_vcs"] = mapping.vcs.abbreviation
759
entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
760
return util.Container(entry)
762
def get_file_changes_uncached(self, entry):
764
old_revid = entry.parents[0].revid
766
old_revid = bzrlib.revision.NULL_REVISION
767
return self.file_changes_for_revision_ids(old_revid, entry.revid)
769
def get_file_changes(self, entry):
770
if self._file_change_cache is None:
771
return self.get_file_changes_uncached(entry)
773
return self._file_change_cache.get_file_changes(entry)
775
def add_changes(self, entry):
776
changes = self.get_file_changes(entry)
777
entry.changes = changes
600
779
def get_file(self, file_id, revid):
601
"returns (filename, data)"
602
inv_entry = self.get_inventory(revid)[file_id]
780
"""Returns (path, filename, file contents)"""
781
inv = self.get_inventory(revid)
782
inv_entry = inv[file_id]
603
783
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
604
return inv_entry.name, rev_tree.get_file_text(file_id)
607
def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
784
path = inv.id2path(file_id)
785
if not path.startswith('/'):
787
return path, inv_entry.name, rev_tree.get_file_text(file_id)
789
def file_changes_for_revision_ids(self, old_revid, new_revid):
609
791
Return a nested data structure containing the changes in a delta::
611
793
added: list((filename, file_id)),
612
794
renamed: list((old_filename, new_filename, file_id)),
613
795
deleted: list((filename, file_id)),
621
type: str('context', 'delete', or 'insert'),
627
if C{get_diffs} is false, the C{chunks} will be omitted.
634
def rich_filename(path, kind):
635
if kind == 'directory':
637
if kind == 'symlink':
641
def process_diff(diff):
644
for line in diff.splitlines():
647
if line.startswith('+++ ') or line.startswith('--- '):
649
if line.startswith('@@ '):
651
if chunk is not None:
653
chunk = util.Container()
655
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
656
old_lineno = lines[0]
657
new_lineno = lines[1]
658
elif line.startswith(' '):
659
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
660
type='context', line=util.html_clean(line[1:])))
663
elif line.startswith('+'):
664
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
665
type='insert', line=util.html_clean(line[1:])))
667
elif line.startswith('-'):
668
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
669
type='delete', line=util.html_clean(line[1:])))
672
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
673
type='unknown', line=util.html_clean(repr(line))))
674
if chunk is not None:
678
def handle_modify(old_path, new_path, fid, kind):
680
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
682
old_lines = old_tree.get_file_lines(fid)
683
new_lines = new_tree.get_file_lines(fid)
685
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
686
diff = buffer.getvalue()
687
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
689
for path, fid, kind in delta.added:
690
added.append((rich_filename(path, kind), fid))
692
for path, fid, kind, text_modified, meta_modified in delta.modified:
693
handle_modify(path, path, fid, kind)
695
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
696
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
697
if meta_modified or text_modified:
698
handle_modify(oldpath, newpath, fid, kind)
700
for path, fid, kind in delta.removed:
701
removed.append((rich_filename(path, kind), fid))
703
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
706
def get_filelist(self, inv, path, sort_type=None):
708
return the list of all files (and their attributes) within a given
711
while path.endswith('/'):
713
if path.startswith('/'):
716
entries = inv.entries()
719
for filepath, entry in entries:
720
fetch_set.add(entry.revision)
721
change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
724
for filepath, entry in entries:
725
if posixpath.dirname(filepath) != path:
727
filename = posixpath.basename(filepath)
728
rich_filename = filename
730
if entry.kind == 'directory':
734
revid = entry.revision
735
change = change_dict[revid]
737
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
738
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
739
file_list.append(file)
741
if sort_type == 'filename':
742
file_list.sort(key=lambda x: x.filename)
743
elif sort_type == 'size':
744
file_list.sort(key=lambda x: x.size)
745
elif sort_type == 'date':
746
file_list.sort(key=lambda x: x.change.date)
749
for file in file_list:
756
_BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b-\x0c\x0e-\x1f]')
759
def annotate_file(self, file_id, revid):
764
file_revid = self.get_inventory(revid)[file_id].revision
767
# because we cache revision metadata ourselves, it's actually much
768
# faster to call 'annotate_iter' on the weave directly than it is to
769
# ask bzrlib to annotate for us.
770
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
773
for line_revid, text in w.annotate_iter(file_revid):
774
revid_set.add(line_revid)
775
if self._BADCHARS_RE.match(text):
776
# bail out; this isn't displayable text
777
yield util.Container(parity=0, lineno=1, status='same',
778
text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
779
change=util.Container())
781
change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
783
last_line_revid = None
784
for line_revid, text in w.annotate_iter(file_revid):
785
if line_revid == last_line_revid:
786
# remember which lines have a new revno and which don't
791
last_line_revid = line_revid
792
change = change_cache[line_revid]
793
trunc_revno = change.revno
794
if len(trunc_revno) > 10:
795
trunc_revno = trunc_revno[:9] + '...'
797
yield util.Container(parity=parity, lineno=lineno, status=status,
798
change=change, text=util.html_clean(text))
801
self.log.debug('annotate: %r secs' % (time.time() - z,))
804
@with_bzrlib_read_lock
805
def get_bundle(self, revid):
806
parents = self._revision_graph[revid]
808
parent_revid = parents[0]
800
text_changes: list((filename, file_id)),
802
repo = self._branch.repository
803
if (bzrlib.revision.is_null(old_revid) or
804
bzrlib.revision.is_null(new_revid)):
805
old_tree, new_tree = map(
806
repo.revision_tree, [old_revid, new_revid])
812
bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, parent_revid, s)
808
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
810
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
812
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
814
return util.Container(
815
added=sorted(reporter.added, key=lambda x: x.filename),
816
renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
817
removed=sorted(reporter.removed, key=lambda x: x.filename),
818
modified=sorted(reporter.modified, key=lambda x: x.filename),
819
text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))