36
from StringIO import StringIO
38
from loggerhead import util
39
from loggerhead.util import decorator
37
from bzrlib import tag
42
38
import bzrlib.branch
43
import bzrlib.bundle.serializer
45
40
import bzrlib.errors
46
import bzrlib.progress
47
42
import bzrlib.revision
51
with_branch_lock = util.with_lock('_lock', 'branch')
55
def with_bzrlib_read_lock(unbound):
56
def bzrlib_read_locked(self, *args, **kw):
57
#self.log.debug('-> %r bzr lock', id(threading.currentThread()))
58
self._branch.repository.lock_read()
60
return unbound(self, *args, **kw)
62
self._branch.repository.unlock()
63
#self.log.debug('<- %r bzr lock', id(threading.currentThread()))
64
return bzrlib_read_locked
67
# bzrlib's UIFactory is not thread-safe
68
uihack = threading.local()
70
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
71
def nested_progress_bar(self):
72
if getattr(uihack, '_progress_bar_stack', None) is None:
73
uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
74
return uihack._progress_bar_stack.get_nested()
76
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
79
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
80
while len(delete_list) < len(insert_list):
81
delete_list.append((None, '', 'context'))
82
while len(insert_list) < len(delete_list):
83
insert_list.append((None, '', 'context'))
84
while len(delete_list) > 0:
85
d = delete_list.pop(0)
86
i = insert_list.pop(0)
87
line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
88
old_line=d[1], new_line=i[1],
89
old_type=d[2], new_type=i[2]))
92
def _make_side_by_side(chunk_list):
94
turn a normal unified-style diff (post-processed by parse_delta) into a
95
side-by-side diff structure. the new structure is::
103
type: str('context' or 'changed'),
108
for chunk in chunk_list:
110
delete_list, insert_list = [], []
111
for line in chunk.diff:
112
if line.type == 'context':
113
if len(delete_list) or len(insert_list):
114
_process_side_by_side_buffers(line_list, delete_list, insert_list)
115
delete_list, insert_list = [], []
116
line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
117
old_line=line.line, new_line=line.line,
118
old_type=line.type, new_type=line.type))
119
elif line.type == 'delete':
120
delete_list.append((line.old_lineno, line.line, line.type))
121
elif line.type == 'insert':
122
insert_list.append((line.new_lineno, line.line, line.type))
123
if len(delete_list) or len(insert_list):
124
_process_side_by_side_buffers(line_list, delete_list, insert_list)
125
out_chunk_list.append(util.Container(diff=line_list))
126
return out_chunk_list
44
from loggerhead import search
45
from loggerhead import util
46
from loggerhead.wholehistory import compute_whole_history_data
129
49
def is_branch(folder):
182
100
def __getitem__(self, index):
183
101
"""Get the date of the index'd item"""
184
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)
186
105
def __len__(self):
187
106
return len(self.revid_list)
190
class History (object):
193
self._change_cache = None
194
self._file_change_cache = None
196
self._lock = threading.RLock()
199
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
202
296
self._branch = branch
203
self._last_revid = self._branch.last_revision()
206
name = self._branch.nick
208
self.log = logging.getLogger('loggerhead.%s' % (name,))
210
graph = branch.repository.get_graph()
211
parent_map = dict(((key, value) for key, value in
212
graph.iter_ancestry([self._last_revid]) if value is not None))
214
self._revision_graph = self._strip_NULL_ghosts(parent_map)
215
self._full_history = []
216
self._revision_info = {}
217
self._revno_revid = {}
218
if bzrlib.revision.is_null(self._last_revid):
219
self._merge_sort = []
221
self._merge_sort = bzrlib.tsort.merge_sort(
222
self._revision_graph, self._last_revid, None,
225
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
226
self._full_history.append(revid)
227
revno_str = '.'.join(str(n) for n in revno)
228
self._revno_revid[revno_str] = revid
229
self._revision_info[revid] = (seq, revid, merge_depth, revno_str,
232
self._where_merged = {}
234
for revid in self._revision_graph.keys():
235
if self._revision_info[revid][2] == 0:
237
for parent in self._revision_graph[revid]:
238
self._where_merged.setdefault(parent, set()).add(revid)
240
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
244
def _strip_NULL_ghosts(revision_graph):
246
Copied over from bzrlib meant as a temporary workaround deprecated
250
# Filter ghosts, and null:
251
if bzrlib.revision.NULL_REVISION in revision_graph:
252
del revision_graph[bzrlib.revision.NULL_REVISION]
253
for key, parents in revision_graph.items():
254
revision_graph[key] = tuple(parent for parent in parents if parent
256
return revision_graph
259
def from_folder(cls, path, name=None):
260
b = bzrlib.branch.Branch.open(path)
263
return cls.from_branch(b, name)
268
def out_of_date(self):
269
# the branch may have been upgraded on disk, in which case we're stale.
270
newly_opened = bzrlib.branch.Branch.open(self._branch.base)
271
if self._branch.__class__ is not \
272
newly_opened.__class__:
274
if self._branch.repository.__class__ is not \
275
newly_opened.repository.__class__:
277
return self._branch.last_revision() != self._last_revid
279
def use_cache(self, cache):
280
self._change_cache = cache
282
def use_file_cache(self, cache):
283
self._file_change_cache = cache
285
def use_search_index(self, index):
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)
289
310
def has_revisions(self):
290
311
return not bzrlib.revision.is_null(self.last_revid)
294
# called when a new history object needs to be created, because the
295
# branch history has changed. we need to immediately close and stop
296
# using our caches, because a new history object will be created to
297
# replace us, using the same cache files.
298
# (may also be called during server shutdown.)
299
if self._change_cache is not None:
300
self._change_cache.close()
301
self._change_cache = None
302
if self._index is not None:
306
def flush_cache(self):
307
if self._change_cache is None:
309
self._change_cache.flush()
311
def check_rebuild(self):
312
if self._change_cache is not None:
313
self._change_cache.check_rebuild()
314
if self._index is not None:
315
self._index.check_rebuild()
317
last_revid = property(lambda self: self._last_revid, None, None)
320
313
def get_config(self):
321
314
return self._branch.get_config()
324
316
def get_revno(self, revid):
325
if revid not in self._revision_info:
317
if revid not in self._rev_indices:
328
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
331
def get_revision_history(self):
332
return self._full_history
320
seq = self._rev_indices[revid]
321
revno = self._rev_info[seq][0][3]
334
324
def get_revids_from(self, revid_list, start_revid):
337
327
revid in revid_list.
339
329
if revid_list is None:
340
revid_list = self._full_history
330
# Just yield the mainline, starting at start_revid
332
is_null = bzrlib.revision.is_null
333
while not is_null(revid):
335
parents = self._rev_info[self._rev_indices[revid]][2]
341
340
revid_set = set(revid_list)
342
341
revid = start_revid
343
343
def introduced_revisions(revid):
345
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
345
seq = self._rev_indices[revid]
346
md = self._rev_info[seq][0][2]
347
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
348
r.add(self._merge_sort[i][1])
348
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
349
r.add(self._rev_info[i][0][1])
352
353
if bzrlib.revision.is_null(revid):
354
if introduced_revisions(revid) & revid_set:
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)
356
parents = self._revision_graph[revid]
361
parents = self._rev_info[self._rev_indices[revid]][2]
357
362
if len(parents) == 0:
359
364
revid = parents[0]
362
366
def get_short_revision_history_by_fileid(self, file_id):
363
# wow. is this really the only way we can get this list? by
364
# man-handling the weave store directly? :-0
365
367
# FIXME: would be awesome if we could get, for a folder, the list of
366
# revisions where items within that folder changed.
367
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
368
w_revids = w.versions()
369
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
373
382
def get_revision_history_since(self, revid_list, date):
374
383
# if a user asks for revisions starting at 01-sep, they mean inclusive,
375
384
# so start at midnight on 02-sep.
376
385
date = date + datetime.timedelta(days=1)
377
# 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...
378
388
revid_list.reverse()
379
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
389
index = bisect.bisect(_RevListToTimestamps(revid_list,
390
self._branch.repository),
382
394
revid_list.reverse()
384
396
return revid_list[index:]
387
def get_revision_history_matching(self, revid_list, text):
388
self.log.debug('searching %d revisions for %r', len(revid_list), text)
390
# this is going to be painfully slow. :(
393
for revid in revid_list:
394
change = self.get_changes([ revid ])[0]
395
if text in change.comment.lower():
397
self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
400
def get_revision_history_matching_indexed(self, revid_list, text):
401
self.log.debug('searching %d revisions for %r', len(revid_list), text)
403
if self._index is None:
404
return self.get_revision_history_matching(revid_list, text)
405
out = self._index.find(text, revid_list)
406
self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
407
# put them in some coherent order :)
408
out = [r for r in self._full_history if r in out]
412
398
def get_search_revid_list(self, query, revid_list):
414
400
given a "quick-search" query, try a few obvious possible meanings:
416
402
- revision id or # ("128.1.3")
417
- 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")
418
405
- comment text as a fallback
420
407
and return a revid list that matches.
605
630
revnol = revno.split(".")
606
631
revnos = ".".join(revnol[:-2])
607
632
revnolast = int(revnol[-1])
608
if d.has_key(revnos):
610
635
if revnolast < m:
611
d[revnos] = ( revnolast, revid )
636
d[revnos] = (revnolast, revid)
613
d[revnos] = ( revnolast, revid )
615
return [ d[revnos][1] for revnos in d.keys() ]
617
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):
619
given a list of changes from L{get_changes}, fill in the branch nicks
620
on all parents and merge points.
644
given a 'change', fill in the branch nicks on all parents and merge
622
647
fetch_set = set()
623
for change in changes:
624
for p in change.parents:
625
fetch_set.add(p.revid)
626
for p in change.merge_points:
627
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)
628
652
p_changes = self.get_changes(list(fetch_set))
629
653
p_change_dict = dict([(c.revid, c) for c in p_changes])
630
for change in changes:
631
# arch-converted branches may not have merged branch info :(
632
for p in change.parents:
633
if p.revid in p_change_dict:
634
p.branch_nick = p_change_dict[p.revid].branch_nick
636
p.branch_nick = '(missing)'
637
for p in change.merge_points:
638
if p.revid in p_change_dict:
639
p.branch_nick = p_change_dict[p.revid].branch_nick
641
p.branch_nick = '(missing)'
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)'
644
665
def get_changes(self, revid_list):
645
666
"""Return a list of changes objects for the given revids.
647
668
Revisions not present and NULL_REVISION will be ignored.
649
if self._change_cache is None:
650
changes = self.get_changes_uncached(revid_list)
652
changes = self._change_cache.get_changes(revid_list)
670
changes = self.get_changes_uncached(revid_list)
653
671
if len(changes) == 0:
656
674
# some data needs to be recalculated each time, because it may
657
675
# change as new revisions are added.
658
676
for change in changes:
659
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
660
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
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]
661
682
if len(change.parents) > 0:
662
if isinstance(change.parents[0], util.Container):
663
# old cache stored a potentially-bogus revno
664
change.parents = [util.Container(revid=p.revid, revno=self.get_revno(p.revid)) for p in change.parents]
666
change.parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in change.parents]
683
change.parents = [util.Container(revid=r,
684
revno=self.get_revno(r)) for r in change.parents]
667
685
change.revno = self.get_revno(change.revid)
676
# alright, let's profile this sucka. (FIXME remove this eventually...)
677
def _get_changes_profiled(self, revid_list):
678
from loggerhead.lsprof import profile
680
ret, stats = profile(self.get_changes_uncached, revid_list)
683
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
684
self.log.info('lsprof complete!')
688
@with_bzrlib_read_lock
689
694
def get_changes_uncached(self, revid_list):
695
# FIXME: deprecated method in getting a null revision
690
696
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
692
repo = self._branch.repository
693
parent_map = repo.get_graph().get_parent_map(revid_list)
698
parent_map = self._branch.repository.get_graph().get_parent_map(
694
700
# We need to return the answer in the same order as the input,
695
701
# less any ghosts.
696
702
present_revids = [revid for revid in revid_list
697
703
if revid in parent_map]
698
rev_list = repo.get_revisions(present_revids)
704
rev_list = self._branch.repository.get_revisions(present_revids)
700
706
return [self._change_from_revision(rev) for rev in rev_list]
702
def _get_deltas_for_revisions_with_trees(self, revisions):
703
"""Produce a list of revision deltas.
705
Note that the input is a sequence of REVISIONS, not revision_ids.
706
Trees will be held in memory until the generator exits.
707
Each delta is relative to the revision's lefthand predecessor.
708
(This is copied from bzrlib.)
710
required_trees = set()
711
for revision in revisions:
712
required_trees.add(revision.revid)
713
required_trees.update([p.revid for p in revision.parents[:1]])
714
trees = dict((t.get_revision_id(), t) for
715
t in self._branch.repository.revision_trees(required_trees))
717
self._branch.repository.lock_read()
719
for revision in revisions:
720
if not revision.parents:
721
old_tree = self._branch.repository.revision_tree(
722
bzrlib.revision.NULL_REVISION)
724
old_tree = trees[revision.parents[0].revid]
725
tree = trees[revision.revid]
726
ret.append(tree.changes_from(old_tree))
729
self._branch.repository.unlock()
731
708
def _change_from_revision(self, revision):
733
710
Given a bzrlib Revision, return a processed "change" for use in
736
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
738
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
740
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])
743
731
'revid': revision.revision_id,
745
'author': revision.committer,
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(),
746
736
'branch_nick': revision.properties.get('branch-nick', None),
747
737
'short_comment': short_message,
748
738
'comment': revision.message,
749
739
'comment_clean': [util.html_clean(s) for s in message],
750
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)
752
760
return util.Container(entry)
754
def get_file_changes_uncached(self, entries):
755
delta_list = self._get_deltas_for_revisions_with_trees(entries)
757
return [self.parse_delta(delta) for delta in delta_list]
760
def get_file_changes(self, entries):
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):
761
770
if self._file_change_cache is None:
762
return self.get_file_changes_uncached(entries)
771
return self.get_file_changes_uncached(entry)
764
return self._file_change_cache.get_file_changes(entries)
766
def add_changes(self, entries):
767
changes_list = self.get_file_changes(entries)
769
for entry, changes in zip(entries, changes_list):
770
entry.changes = changes
773
def get_change_with_diff(self, revid, compare_revid=None):
774
change = self.get_changes([revid])[0]
776
if compare_revid is None:
778
compare_revid = change.parents[0].revid
780
compare_revid = 'null:'
782
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
783
rev_tree2 = self._branch.repository.revision_tree(revid)
784
delta = rev_tree2.changes_from(rev_tree1)
786
change.changes = self.parse_delta(delta)
787
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
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
792
779
def get_file(self, file_id, revid):
793
"returns (path, filename, data)"
780
"""Returns (path, filename, file contents)"""
794
781
inv = self.get_inventory(revid)
795
782
inv_entry = inv[file_id]
796
783
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
799
786
path = '/' + path
800
787
return path, inv_entry.name, rev_tree.get_file_text(file_id)
802
def _parse_diffs(self, old_tree, new_tree, delta):
804
Return a list of processed diffs, in the format::
813
type: str('context', 'delete', or 'insert'),
822
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
824
process.append((old_path, new_path, fid, kind))
825
for path, fid, kind, text_modified, meta_modified in delta.modified:
826
process.append((path, path, fid, kind))
828
for old_path, new_path, fid, kind in process:
829
old_lines = old_tree.get_file_lines(fid)
830
new_lines = new_tree.get_file_lines(fid)
832
if old_lines != new_lines:
834
bzrlib.diff.internal_diff(old_path, old_lines,
835
new_path, new_lines, buffer)
836
except bzrlib.errors.BinaryFile:
839
diff = buffer.getvalue()
842
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
846
def _process_diff(self, diff):
847
# doesn't really need to be a method; could be static.
850
for line in diff.splitlines():
853
if line.startswith('+++ ') or line.startswith('--- '):
855
if line.startswith('@@ '):
857
if chunk is not None:
859
chunk = util.Container()
861
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
862
old_lineno = lines[0]
863
new_lineno = lines[1]
864
elif line.startswith(' '):
865
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
866
type='context', line=util.fixed_width(line[1:])))
869
elif line.startswith('+'):
870
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
871
type='insert', line=util.fixed_width(line[1:])))
873
elif line.startswith('-'):
874
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
875
type='delete', line=util.fixed_width(line[1:])))
878
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
879
type='unknown', line=util.fixed_width(repr(line))))
880
if chunk is not None:
884
def parse_delta(self, delta):
789
def file_changes_for_revision_ids(self, old_revid, new_revid):
886
791
Return a nested data structure containing the changes in a delta::
901
for path, fid, kind in delta.added:
902
added.append((rich_filename(path, kind), fid))
904
for path, fid, kind, text_modified, meta_modified in delta.modified:
905
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
907
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
908
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
909
if meta_modified or text_modified:
910
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
912
for path, fid, kind in delta.removed:
913
removed.append((rich_filename(path, kind), fid))
915
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
918
def add_side_by_side(changes):
919
# FIXME: this is a rotten API.
920
for change in changes:
921
for m in change.changes.modified:
922
m.sbs_chunks = _make_side_by_side(m.chunks)
925
def get_filelist(self, inv, file_id, sort_type=None):
927
return the list of all files (and their attributes) within a given
931
dir_ie = inv[file_id]
932
path = inv.id2path(file_id)
937
for filename, entry in dir_ie.children.iteritems():
938
revid_set.add(entry.revision)
941
for change in self.get_changes(list(revid_set)):
942
change_dict[change.revid] = change
944
for filename, entry in dir_ie.children.iteritems():
946
if entry.kind == 'directory':
949
revid = entry.revision
951
file = util.Container(
952
filename=filename, executable=entry.executable, kind=entry.kind,
953
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
954
revid=revid, change=change_dict[revid])
955
file_list.append(file)
957
if sort_type == 'filename' or sort_type is None:
958
file_list.sort(key=lambda x: x.filename)
959
elif sort_type == 'size':
960
file_list.sort(key=lambda x: x.size)
961
elif sort_type == 'date':
962
file_list.sort(key=lambda x: x.change.date)
965
for file in file_list:
972
_BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
975
def annotate_file(self, file_id, revid):
980
file_revid = self.get_inventory(revid)[file_id].revision
982
tree = self._branch.repository.revision_tree(file_revid)
985
for line_revid, text in tree.annotate_iter(file_id):
986
revid_set.add(line_revid)
987
if self._BADCHARS_RE.match(text):
988
# bail out; this isn't displayable text
989
yield util.Container(parity=0, lineno=1, status='same',
990
text='(This is a binary file.)',
991
change=util.Container())
993
change_cache = dict([(c.revid, c) \
994
for c in self.get_changes(list(revid_set))])
996
last_line_revid = None
997
for line_revid, text in tree.annotate_iter(file_id):
998
if line_revid == last_line_revid:
999
# remember which lines have a new revno and which don't
1004
last_line_revid = line_revid
1005
change = change_cache[line_revid]
1006
trunc_revno = change.revno
1007
if len(trunc_revno) > 10:
1008
trunc_revno = trunc_revno[:9] + '...'
1010
yield util.Container(parity=parity, lineno=lineno, status=status,
1011
change=change, text=util.fixed_width(text))
1014
self.log.debug('annotate: %r secs' % (time.time() - z,))
1017
def get_bundle(self, revid, compare_revid=None):
1018
if compare_revid is None:
1019
parents = self._revision_graph[revid]
1020
if len(parents) > 0:
1021
compare_revid = parents[0]
1023
compare_revid = None
1025
bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)
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])
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))