41
from StringIO import StringIO
37
from loggerhead import search
38
43
from loggerhead import util
39
from loggerhead.wholehistory import compute_whole_history_data
44
from loggerhead.util import decorator
47
import bzrlib.annotate
42
48
import bzrlib.branch
49
import bzrlib.bundle.serializer
50
import bzrlib.decorators
44
52
import bzrlib.errors
53
import bzrlib.progress
54
import bzrlib.revision
55
import bzrlib.textfile
60
with_branch_lock = util.with_lock('_lock', 'branch')
63
# bzrlib's UIFactory is not thread-safe
64
uihack = threading.local()
66
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
67
def nested_progress_bar(self):
68
if getattr(uihack, '_progress_bar_stack', None) is None:
69
uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
70
return uihack._progress_bar_stack.get_nested()
72
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
75
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
76
while len(delete_list) < len(insert_list):
77
delete_list.append((None, '', 'context'))
78
while len(insert_list) < len(delete_list):
79
insert_list.append((None, '', 'context'))
80
while len(delete_list) > 0:
81
d = delete_list.pop(0)
82
i = insert_list.pop(0)
83
line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
84
old_line=d[1], new_line=i[1],
85
old_type=d[2], new_type=i[2]))
88
def _make_side_by_side(chunk_list):
90
turn a normal unified-style diff (post-processed by parse_delta) into a
91
side-by-side diff structure. the new structure is::
99
type: str('context' or 'changed'),
104
for chunk in chunk_list:
106
delete_list, insert_list = [], []
107
for line in chunk.diff:
108
if line.type == 'context':
109
if len(delete_list) or len(insert_list):
110
_process_side_by_side_buffers(line_list, delete_list, insert_list)
111
delete_list, insert_list = [], []
112
line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
113
old_line=line.line, new_line=line.line,
114
old_type=line.type, new_type=line.type))
115
elif line.type == 'delete':
116
delete_list.append((line.old_lineno, line.line, line.type))
117
elif line.type == 'insert':
118
insert_list.append((line.new_lineno, line.line, line.type))
119
if len(delete_list) or len(insert_list):
120
_process_side_by_side_buffers(line_list, delete_list, insert_list)
121
out_chunk_list.append(util.Container(diff=line_list))
122
return out_chunk_list
47
125
def is_branch(folder):
98
178
def __getitem__(self, index):
99
179
"""Get the date of the index'd item"""
100
return datetime.datetime.fromtimestamp(self.repository.get_revision(
101
self.revid_list[index]).timestamp)
180
return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
103
182
def __len__(self):
104
183
return len(self.revid_list)
106
class FileChangeReporter(object):
107
def __init__(self, old_inv, new_inv):
112
self.text_changes = []
113
self.old_inv = old_inv
114
self.new_inv = new_inv
116
def revid(self, inv, file_id):
118
return inv[file_id].revision
119
except bzrlib.errors.NoSuchId:
122
def report(self, file_id, paths, versioned, renamed, modified,
124
if modified not in ('unchanged', 'kind changed'):
125
if versioned == 'removed':
126
filename = rich_filename(paths[0], kind[0])
128
filename = rich_filename(paths[1], kind[1])
129
self.text_changes.append(util.Container(
130
filename=filename, file_id=file_id,
131
old_revision=self.revid(self.old_inv, file_id),
132
new_revision=self.revid(self.new_inv, file_id)))
133
if versioned == 'added':
134
self.added.append(util.Container(
135
filename=rich_filename(paths[1], kind),
136
file_id=file_id, kind=kind[1]))
137
elif versioned == 'removed':
138
self.removed.append(util.Container(
139
filename=rich_filename(paths[0], kind),
140
file_id=file_id, kind=kind[0]))
142
self.renamed.append(util.Container(
143
old_filename=rich_filename(paths[0], kind[0]),
144
new_filename=rich_filename(paths[1], kind[1]),
146
text_modified=modified == 'modified'))
148
self.modified.append(util.Container(
149
filename=rich_filename(paths[1], kind),
153
class RevInfoMemoryCache(object):
154
"""A store that validates values against the revids they were stored with.
156
We use a unique key for each branch.
158
The reason for not just using the revid as the key is so that when a new
159
value is provided for a branch, we replace the old value used for the
162
There is another implementation of the same interface in
163
loggerhead.changecache.RevInfoDiskCache.
166
def __init__(self, cache):
169
def get(self, key, revid):
170
"""Return the data associated with `key`, subject to a revid check.
172
If a value was stored under `key`, with the same revid, return it.
173
Otherwise return None.
175
cached = self._cache.get(key)
178
stored_revid, data = cached
179
if revid == stored_revid:
184
def set(self, key, revid, data):
185
"""Store `data` under `key`, to be checked against `revid` on get().
187
self._cache[key] = (revid, data)
190
186
class History (object):
191
"""Decorate a branch to provide information for rendering.
193
History objects are expected to be short lived -- when serving a request
194
for a particular branch, open it, read-lock it, wrap a History object
195
around it, serve the request, throw the History object away, unlock the
196
branch and throw it away.
198
:ivar _file_change_cache: An object that caches information about the
199
files that changed between two revisions.
200
:ivar _rev_info: A list of information about revisions. This is by far
201
the most cryptic data structure in loggerhead. At the top level, it
202
is a list of 3-tuples [(merge-info, where-merged, parents)].
203
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
204
like a merged sorted list, but the revno is stringified.
205
`where-merged` is a tuple of revisions that have this revision as a
206
non-lefthand parent. Finally, `parents` is just the usual list of
207
parents of this revision.
208
:ivar _rev_indices: A dictionary mapping each revision id to the index of
209
the information about it in _rev_info.
210
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
214
def _load_whole_history_data(self, caches, cache_key):
215
"""Set the attributes relating to the whole history of the branch.
217
:param caches: a list of caches with interfaces like
218
`RevInfoMemoryCache` and be ordered from fastest to slowest.
219
:param cache_key: the key to use with the caches.
221
self._rev_indices = None
222
self._rev_info = None
225
def update_missed_caches():
226
for cache in missed_caches:
227
cache.set(cache_key, self.last_revid, self._rev_info)
229
data = cache.get(cache_key, self.last_revid)
231
self._rev_info = data
232
update_missed_caches()
235
missed_caches.append(cache)
237
whole_history_data = compute_whole_history_data(self._branch)
238
self._rev_info, self._rev_indices = whole_history_data
239
update_missed_caches()
241
if self._rev_indices is not None:
242
self._revno_revid = {}
243
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
244
self._revno_revid[revno_str] = revid
246
self._revno_revid = {}
247
self._rev_indices = {}
248
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
249
self._rev_indices[revid] = seq
250
self._revno_revid[revno_str] = revid
252
def __init__(self, branch, whole_history_data_cache, file_cache=None,
253
revinfo_disk_cache=None, cache_key=None):
254
assert branch.is_locked(), (
255
"Can only construct a History object with a read-locked branch.")
256
if file_cache is not None:
257
self._file_change_cache = file_cache
258
file_cache.history = self
260
self._file_change_cache = None
189
self._change_cache = None
190
self._file_change_cache = None
192
self._lock = threading.RLock()
195
def from_branch(cls, branch, name=None):
261
198
self._branch = branch
262
self._inventory_cache = {}
263
self._branch_nick = self._branch.get_config().get_nickname()
264
self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
266
self.last_revid = branch.last_revision()
268
caches = [RevInfoMemoryCache(whole_history_data_cache)]
269
if revinfo_disk_cache:
270
caches.append(revinfo_disk_cache)
271
self._load_whole_history_data(caches, cache_key)
274
def has_revisions(self):
275
return not bzrlib.revision.is_null(self.last_revid)
199
self._last_revid = self._branch.last_revision()
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,))
210
self._full_history = []
211
self._revision_info = {}
212
self._revno_revid = {}
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
215
self._full_history.append(revid)
216
revno_str = '.'.join(str(n) for n in revno)
217
self._revno_revid[revno_str] = revid
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
221
self._where_merged = {}
222
for revid in self._revision_graph.keys():
223
if not revid in self._full_history:
225
for parent in self._revision_graph[revid]:
226
self._where_merged.setdefault(parent, set()).add(revid)
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
232
def from_folder(cls, path, name=None):
233
b = bzrlib.branch.Branch.open(path)
234
return cls.from_branch(b, name)
237
def out_of_date(self):
238
# the branch may have been upgraded on disk, in which case we're stale.
239
if self._branch.__class__ is not \
240
bzrlib.branch.Branch.open(self._branch.base).__class__:
242
return self._branch.last_revision() != self._last_revid
244
def use_cache(self, cache):
245
self._change_cache = cache
247
def use_file_cache(self, cache):
248
self._file_change_cache = cache
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()
278
last_revid = property(lambda self: self._last_revid, None, None)
277
281
def get_config(self):
278
282
return self._branch.get_config()
280
284
def get_revno(self, revid):
281
if revid not in self._rev_indices:
285
if revid not in self._revision_info:
284
seq = self._rev_indices[revid]
285
revno = self._rev_info[seq][0][3]
288
def get_revids_from(self, revid_list, start_revid):
290
Yield the mainline (wrt start_revid) revisions that merged each
293
if revid_list is None:
294
revid_list = [r[0][1] for r in self._rev_info]
295
revid_set = set(revid_list)
298
def introduced_revisions(revid):
300
seq = self._rev_indices[revid]
301
md = self._rev_info[seq][0][2]
303
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
304
r.add(self._rev_info[i][0][1])
308
if bzrlib.revision.is_null(revid):
310
if introduced_revisions(revid) & revid_set:
288
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
291
def get_revision_history(self):
292
return self._full_history
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):
312
parents = self._rev_info[self._rev_indices[revid]][2]
303
if not self._revision_graph.has_key(revid):
305
parents = self._revision_graph[revid]
313
306
if len(parents) == 0:
315
308
revid = parents[0]
317
311
def get_short_revision_history_by_fileid(self, file_id):
312
# wow. is this really the only way we can get this list? by
313
# man-handling the weave store directly? :-0
318
314
# FIXME: would be awesome if we could get, for a folder, the list of
319
# revisions where items within that folder changed.i
320
possible_keys = [(file_id, revid) for revid in self._rev_indices]
321
get_parent_map = self._branch.repository.texts.get_parent_map
322
# We chunk the requests as this works better with GraphIndex.
323
# See _filter_revisions_touching_file_id in bzrlib/log.py
324
# for more information.
327
for start in xrange(0, len(possible_keys), chunk_size):
328
next_keys = possible_keys[start:start + chunk_size]
329
revids += [k[1] for k in get_parent_map(next_keys)]
330
del possible_keys, next_keys
315
# revisions where items within that folder changed.
316
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
317
w_revids = w.versions()
318
revids = [r for r in self._full_history if r in w_revids]
333
322
def get_revision_history_since(self, revid_list, date):
334
323
# if a user asks for revisions starting at 01-sep, they mean inclusive,
335
324
# so start at midnight on 02-sep.
336
325
date = date + datetime.timedelta(days=1)
337
# our revid list is sorted in REVERSE date order,
338
# so go thru some hoops here...
326
# our revid list is sorted in REVERSE date order, so go thru some hoops here...
339
327
revid_list.reverse()
340
index = bisect.bisect(_RevListToTimestamps(revid_list,
341
self._branch.repository),
328
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
345
331
revid_list.reverse()
347
333
return revid_list[index:]
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]
349
361
def get_search_revid_list(self, query, revid_list):
351
363
given a "quick-search" query, try a few obvious possible meanings:
353
365
- revision id or # ("128.1.3")
354
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
355
iso style "yyyy-mm-dd")
366
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
356
367
- comment text as a fallback
358
369
and return a revid list that matches.
547
552
revnol = revno.split(".")
548
553
revnos = ".".join(revnol[:-2])
549
554
revnolast = int(revnol[-1])
550
if revnos in d.keys():
555
if d.has_key(revnos):
552
557
if revnolast < m:
553
d[revnos] = (revnolast, revid)
558
d[revnos] = ( revnolast, revid )
555
d[revnos] = (revnolast, revid)
557
return [d[revnos][1] for revnos in d.keys()]
559
def add_branch_nicks(self, change):
560
d[revnos] = ( revnolast, revid )
562
return [ d[revnos][1] for revnos in d.keys() ]
564
def get_branch_nicks(self, changes):
561
given a 'change', fill in the branch nicks on all parents and merge
566
given a list of changes from L{get_changes}, fill in the branch nicks
567
on all parents and merge points.
564
569
fetch_set = set()
565
for p in change.parents:
566
fetch_set.add(p.revid)
567
for p in change.merge_points:
568
fetch_set.add(p.revid)
570
for change in changes:
571
for p in change.parents:
572
fetch_set.add(p.revid)
573
for p in change.merge_points:
574
fetch_set.add(p.revid)
569
575
p_changes = self.get_changes(list(fetch_set))
570
576
p_change_dict = dict([(c.revid, c) for c in p_changes])
571
for p in change.parents:
572
if p.revid in p_change_dict:
573
p.branch_nick = p_change_dict[p.revid].branch_nick
575
p.branch_nick = '(missing)'
576
for p in change.merge_points:
577
if p.revid in p_change_dict:
578
p.branch_nick = p_change_dict[p.revid].branch_nick
580
p.branch_nick = '(missing)'
577
for change in changes:
578
# arch-converted branches may not have merged branch info :(
579
for p in change.parents:
580
if p.revid in p_change_dict:
581
p.branch_nick = p_change_dict[p.revid].branch_nick
583
p.branch_nick = '(missing)'
584
for p in change.merge_points:
585
if p.revid in p_change_dict:
586
p.branch_nick = p_change_dict[p.revid].branch_nick
588
p.branch_nick = '(missing)'
582
591
def get_changes(self, revid_list):
583
"""Return a list of changes objects for the given revids.
585
Revisions not present and NULL_REVISION will be ignored.
587
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)
588
596
if len(changes) == 0:
591
599
# some data needs to be recalculated each time, because it may
592
600
# change as new revisions are added.
593
601
for change in changes:
594
merge_revids = self.simplify_merge_point_list(
595
self.get_merge_point_list(change.revid))
596
change.merge_points = [
597
util.Container(revid=r,
598
revno=self.get_revno(r)) for r in merge_revids]
599
if len(change.parents) > 0:
600
change.parents = [util.Container(revid=r,
601
revno=self.get_revno(r)) for r in change.parents]
602
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
603
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
602
604
change.revno = self.get_revno(change.revid)
611
def get_changes_uncached(self, revid_list):
612
# FIXME: deprecated method in getting a null revision
613
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
615
parent_map = self._branch.repository.get_graph().get_parent_map(
617
# We need to return the answer in the same order as the input,
619
present_revids = [revid for revid in revid_list
620
if revid in parent_map]
621
rev_list = self._branch.repository.get_revisions(present_revids)
623
return [self._change_from_revision(rev) for rev in rev_list]
625
def _change_from_revision(self, revision):
627
Given a bzrlib Revision, return a processed "change" for use in
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.
627
Note that the input is a sequence of REVISIONS, not revision_ids.
628
Trees will be held in memory until the generator exits.
629
Each delta is relative to the revision's lefthand predecessor.
631
required_trees = set()
632
for entry in entries:
633
required_trees.add(entry.revid)
634
required_trees.update([p.revid for p in entry.parents[:1]])
635
trees = dict((t.get_revision_id(), t) for
636
t in self._branch.repository.revision_trees(required_trees))
638
self._branch.repository.lock_read()
640
for entry in entries:
641
if not entry.parents:
642
old_tree = self._branch.repository.revision_tree(
643
bzrlib.revision.NULL_REVISION)
645
old_tree = trees[entry.parents[0].revid]
646
tree = trees[entry.revid]
647
ret.append(tree.changes_from(old_tree))
650
self._branch.repository.unlock()
652
def entry_from_revision(self, revision):
653
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
655
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
630
657
message, short_message = clean_message(revision.message)
632
tags = self._branch.tags.get_reverse_tag_dict()
635
if tags.has_key(revision.revision_id):
636
revtags = ', '.join(tags[revision.revision_id])
639
660
'revid': revision.revision_id,
640
'date': datetime.datetime.fromtimestamp(revision.timestamp),
641
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
642
'authors': revision.get_apparent_authors(),
662
'author': revision.committer,
643
663
'branch_nick': revision.properties.get('branch-nick', None),
644
664
'short_comment': short_message,
645
665
'comment': revision.message,
646
666
'comment_clean': [util.html_clean(s) for s in message],
647
'parents': revision.parent_ids,
648
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
651
669
return util.Container(entry)
653
def get_file_changes_uncached(self, entry):
655
old_revid = entry.parents[0].revid
657
old_revid = bzrlib.revision.NULL_REVISION
658
return self.file_changes_for_revision_ids(old_revid, entry.revid)
660
def get_file_changes(self, 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()
692
def get_file_changes_uncached(self, entries):
693
delta_list = self._get_deltas_for_revisions_with_trees(entries)
695
return [self.parse_delta(delta) for delta in delta_list]
698
def get_file_changes(self, entries):
661
699
if self._file_change_cache is None:
662
return self.get_file_changes_uncached(entry)
700
return self.get_file_changes_uncached(entries)
664
return self._file_change_cache.get_file_changes(entry)
666
def add_changes(self, entry):
667
changes = self.get_file_changes(entry)
668
entry.changes = changes
702
return self._file_change_cache.get_file_changes(entries)
704
def add_changes(self, entries):
705
changes_list = self.get_file_changes(entries)
707
for entry, changes in zip(entries, changes_list):
708
entry.changes = changes
711
def get_change_with_diff(self, revid, compare_revid=None):
712
entry = self.get_changes([revid])[0]
714
if compare_revid is None:
716
compare_revid = entry.parents[0].revid
718
compare_revid = 'null:'
720
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
721
rev_tree2 = self._branch.repository.revision_tree(revid)
722
delta = rev_tree2.changes_from(rev_tree1)
724
entry.changes = self.parse_delta(delta)
726
entry.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
670
731
def get_file(self, file_id, revid):
671
732
"returns (path, filename, data)"
672
733
inv = self.get_inventory(revid)
677
738
path = '/' + path
678
739
return path, inv_entry.name, rev_tree.get_file_text(file_id)
680
def file_changes_for_revision_ids(self, old_revid, new_revid):
741
def _parse_diffs(self, old_tree, new_tree, delta):
743
Return a list of processed diffs, in the format::
752
type: str('context', 'delete', or 'insert'),
761
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
763
process.append((old_path, new_path, fid, kind))
764
for path, fid, kind, text_modified, meta_modified in delta.modified:
765
process.append((path, path, fid, kind))
767
for old_path, new_path, fid, kind in process:
768
old_lines = old_tree.get_file_lines(fid)
769
new_lines = new_tree.get_file_lines(fid)
771
if old_lines != new_lines:
773
bzrlib.diff.internal_diff(old_path, old_lines,
774
new_path, new_lines, buffer)
775
except bzrlib.errors.BinaryFile:
778
diff = buffer.getvalue()
781
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff)))
785
def _process_diff(self, diff):
786
# doesn't really need to be a method; could be static.
789
for line in diff.splitlines():
792
if line.startswith('+++ ') or line.startswith('--- '):
794
if line.startswith('@@ '):
796
if chunk is not None:
798
chunk = util.Container()
800
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
801
old_lineno = lines[0]
802
new_lineno = lines[1]
803
elif line.startswith(' '):
804
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
805
type='context', line=util.fixed_width(line[1:])))
808
elif line.startswith('+'):
809
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
810
type='insert', line=util.fixed_width(line[1:])))
812
elif line.startswith('-'):
813
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
814
type='delete', line=util.fixed_width(line[1:])))
817
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
818
type='unknown', line=util.fixed_width(repr(line))))
819
if chunk is not None:
823
def parse_delta(self, delta):
682
825
Return a nested data structure containing the changes in a delta::
691
text_changes: list((filename, file_id)),
693
repo = self._branch.repository
694
if bzrlib.revision.is_null(old_revid) or \
695
bzrlib.revision.is_null(new_revid):
696
old_tree, new_tree = map(
697
repo.revision_tree, [old_revid, new_revid])
699
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
701
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
703
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
705
return util.Container(
706
added=sorted(reporter.added, key=lambda x:x.filename),
707
renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
708
removed=sorted(reporter.removed, key=lambda x:x.filename),
709
modified=sorted(reporter.modified, key=lambda x:x.filename),
710
text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))
840
for path, fid, kind in delta.added:
841
added.append((rich_filename(path, kind), fid))
843
for path, fid, kind, text_modified, meta_modified in delta.modified:
844
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
846
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
847
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
848
if meta_modified or text_modified:
849
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
851
for path, fid, kind in delta.removed:
852
removed.append((rich_filename(path, kind), fid))
854
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
857
def add_side_by_side(changes):
858
# FIXME: this is a rotten API.
859
for change in changes:
860
for m in change.changes.modified:
861
m.sbs_chunks = _make_side_by_side(m.chunks)
864
def get_filelist(self, inv, file_id, sort_type=None):
866
return the list of all files (and their attributes) within a given
870
dir_ie = inv[file_id]
871
path = inv.id2path(file_id)
876
for filename, entry in dir_ie.children.iteritems():
877
revid_set.add(entry.revision)
880
for change in self.get_changes(list(revid_set)):
881
change_dict[change.revid] = change
883
for filename, entry in dir_ie.children.iteritems():
885
if entry.kind == 'directory':
888
revid = entry.revision
890
file = util.Container(
891
filename=filename, executable=entry.executable, kind=entry.kind,
892
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
893
revid=revid, change=change_dict[revid])
894
file_list.append(file)
896
if sort_type == 'filename' or sort_type is None:
897
file_list.sort(key=lambda x: x.filename)
898
elif sort_type == 'size':
899
file_list.sort(key=lambda x: x.size)
900
elif sort_type == 'date':
901
file_list.sort(key=lambda x: x.change.date)
904
for file in file_list:
911
_BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
914
def annotate_file(self, file_id, revid):
919
file_revid = self.get_inventory(revid)[file_id].revision
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())
928
for line_revid, text in w.annotate_iter(file_revid):
929
revid_set.add(line_revid)
930
if self._BADCHARS_RE.match(text):
931
# bail out; this isn't displayable text
932
yield util.Container(parity=0, lineno=1, status='same',
933
text='(This is a binary file.)',
934
change=util.Container())
936
change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
938
last_line_revid = None
939
for line_revid, text in w.annotate_iter(file_revid):
940
if line_revid == last_line_revid:
941
# remember which lines have a new revno and which don't
946
last_line_revid = line_revid
947
change = change_cache[line_revid]
948
trunc_revno = change.revno
949
if len(trunc_revno) > 10:
950
trunc_revno = trunc_revno[:9] + '...'
952
yield util.Container(parity=parity, lineno=lineno, status=status,
953
change=change, text=util.fixed_width(text))
956
self.log.debug('annotate: %r secs' % (time.time() - z,))
959
def get_bundle(self, revid, compare_revid=None):
960
if compare_revid is None:
961
parents = self._revision_graph[revid]
963
compare_revid = parents[0]
967
bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)