39
41
from StringIO import StringIO
41
from loggerhead import search
42
43
from loggerhead import util
43
from loggerhead.wholehistory import compute_whole_history_data
44
from loggerhead.util import decorator
47
import bzrlib.annotate
46
48
import bzrlib.branch
49
import bzrlib.bundle.serializer
50
import bzrlib.decorators
49
52
import bzrlib.errors
50
53
import bzrlib.progress
51
import bzrlib.revision
52
54
import bzrlib.textfile
53
55
import bzrlib.tsort
59
with_branch_lock = util.with_lock('_lock', 'branch')
62
def with_bzrlib_read_lock(unbound):
63
def bzrlib_read_locked(self, *args, **kw):
64
#self.log.debug('-> %r bzr lock', id(threading.currentThread()))
65
self._branch.repository.lock_read()
67
return unbound(self, *args, **kw)
69
self._branch.repository.unlock()
70
#self.log.debug('<- %r bzr lock', id(threading.currentThread()))
71
return bzrlib_read_locked
56
74
# bzrlib's UIFactory is not thread-safe
57
75
uihack = threading.local()
60
77
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
62
78
def nested_progress_bar(self):
63
79
if getattr(uihack, '_progress_bar_stack', None) is None:
64
pbs = bzrlib.progress.ProgressBarStack(
65
klass=bzrlib.progress.DummyProgress)
66
uihack._progress_bar_stack = pbs
80
uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
67
81
return uihack._progress_bar_stack.get_nested()
69
83
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
86
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
87
while len(delete_list) < len(insert_list):
88
delete_list.append((None, '', 'context'))
89
while len(insert_list) < len(delete_list):
90
insert_list.append((None, '', 'context'))
91
while len(delete_list) > 0:
92
d = delete_list.pop(0)
93
i = insert_list.pop(0)
94
line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
95
old_line=d[1], new_line=i[1],
96
old_type=d[2], new_type=i[2]))
99
def _make_side_by_side(chunk_list):
101
turn a normal unified-style diff (post-processed by parse_delta) into a
102
side-by-side diff structure. the new structure is::
110
type: str('context' or 'changed'),
115
for chunk in chunk_list:
117
delete_list, insert_list = [], []
118
for line in chunk.diff:
119
if line.type == 'context':
120
if len(delete_list) or len(insert_list):
121
_process_side_by_side_buffers(line_list, delete_list, insert_list)
122
delete_list, insert_list = [], []
123
line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
124
old_line=line.line, new_line=line.line,
125
old_type=line.type, new_type=line.type))
126
elif line.type == 'delete':
127
delete_list.append((line.old_lineno, line.line, line.type))
128
elif line.type == 'insert':
129
insert_list.append((line.new_lineno, line.line, line.type))
130
if len(delete_list) or len(insert_list):
131
_process_side_by_side_buffers(line_list, delete_list, insert_list)
132
out_chunk_list.append(util.Container(diff=line_list))
133
return out_chunk_list
71
136
def is_branch(folder):
73
138
bzrlib.branch.Branch.open(folder)
125
172
def __getitem__(self, index):
126
173
"""Get the date of the index'd item"""
127
return datetime.datetime.fromtimestamp(self.repository.get_revision(
128
self.revid_list[index]).timestamp)
174
return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
130
176
def __len__(self):
131
177
return len(self.revid_list)
133
class FileChangeReporter(object):
134
def __init__(self, old_inv, new_inv):
139
self.text_changes = []
140
self.old_inv = old_inv
141
self.new_inv = new_inv
143
def revid(self, inv, file_id):
145
return inv[file_id].revision
146
except bzrlib.errors.NoSuchId:
149
def report(self, file_id, paths, versioned, renamed, modified,
151
if modified not in ('unchanged', 'kind changed'):
152
if versioned == 'removed':
153
filename = rich_filename(paths[0], kind[0])
155
filename = rich_filename(paths[1], kind[1])
156
self.text_changes.append(util.Container(
157
filename=filename, file_id=file_id,
158
old_revision=self.revid(self.old_inv, file_id),
159
new_revision=self.revid(self.new_inv, file_id)))
160
if versioned == 'added':
161
self.added.append(util.Container(
162
filename=rich_filename(paths[1], kind),
163
file_id=file_id, kind=kind[1]))
164
elif versioned == 'removed':
165
self.removed.append(util.Container(
166
filename=rich_filename(paths[0], kind),
167
file_id=file_id, kind=kind[0]))
169
self.renamed.append(util.Container(
170
old_filename=rich_filename(paths[0], kind[0]),
171
new_filename=rich_filename(paths[1], kind[1]),
173
text_modified=modified == 'modified'))
175
self.modified.append(util.Container(
176
filename=rich_filename(paths[1], kind),
180
180
class History (object):
181
"""Decorate a branch to provide information for rendering.
183
History objects are expected to be short lived -- when serving a request
184
for a particular branch, open it, read-lock it, wrap a History object
185
around it, serve the request, throw the History object away, unlock the
186
branch and throw it away.
188
:ivar _file_change_cache: xx
191
def __init__(self, branch, whole_history_data_cache):
192
assert branch.is_locked(), (
193
"Can only construct a History object with a read-locked branch.")
194
self._file_change_cache = None
183
self._change_cache = None
185
self._lock = threading.RLock()
188
def from_branch(cls, branch, name=None):
195
191
self._branch = branch
196
self._inventory_cache = {}
197
self._branch_nick = self._branch.get_config().get_nickname()
198
self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
200
self.last_revid = branch.last_revision()
202
whole_history_data = whole_history_data_cache.get(self.last_revid)
203
if whole_history_data is None:
204
whole_history_data = compute_whole_history_data(branch)
205
whole_history_data_cache[self.last_revid] = whole_history_data
207
(self._revision_graph, self._full_history, self._revision_info,
208
self._revno_revid, self._merge_sort, self._where_merged,
209
) = whole_history_data
211
def use_file_cache(self, cache):
212
self._file_change_cache = cache
215
def has_revisions(self):
216
return not bzrlib.revision.is_null(self.last_revid)
192
self._history = branch.revision_history()
193
self._last_revid = self._history[-1]
194
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
197
name = self._branch.nick
199
self.log = logging.getLogger('loggerhead.%s' % (name,))
201
self._full_history = []
202
self._revision_info = {}
203
self._revno_revid = {}
204
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
206
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
207
self._full_history.append(revid)
208
revno_str = '.'.join(str(n) for n in revno)
209
self._revno_revid[revno_str] = revid
210
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
215
self._where_merged = {}
216
for revid in self._revision_graph.keys():
217
if not revid in self._full_history:
219
for parent in self._revision_graph[revid]:
220
self._where_merged.setdefault(parent, set()).add(revid)
222
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
226
def from_folder(cls, path, name=None):
227
b = bzrlib.branch.Branch.open(path)
228
return cls.from_branch(b, name)
231
def out_of_date(self):
232
if self._branch.revision_history()[-1] != self._last_revid:
236
def use_cache(self, cache):
237
self._change_cache = cache
239
def use_search_index(self, index):
244
# called when a new history object needs to be created, because the
245
# branch history has changed. we need to immediately close and stop
246
# using our caches, because a new history object will be created to
247
# replace us, using the same cache files.
248
# (may also be called during server shutdown.)
249
if self._change_cache is not None:
250
self._change_cache.close()
251
self._change_cache = None
252
if self._index is not None:
256
def flush_cache(self):
257
if self._change_cache is None:
259
self._change_cache.flush()
261
def check_rebuild(self):
262
if self._change_cache is not None:
263
self._change_cache.check_rebuild()
264
if self._index is not None:
265
self._index.check_rebuild()
267
last_revid = property(lambda self: self._last_revid, None, None)
269
count = property(lambda self: self._count, None, None)
218
272
def get_config(self):
219
273
return self._branch.get_config()
276
def get_revision(self, revid):
277
return self._branch.repository.get_revision(revid)
221
279
def get_revno(self, revid):
222
280
if revid not in self._revision_info:
225
(seq, revid, merge_depth,
226
revno_str, end_of_merge) = self._revision_info[revid]
283
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
229
def get_revids_from(self, revid_list, start_revid):
231
Yield the mainline (wrt start_revid) revisions that merged each
234
if revid_list is None:
235
revid_list = self._full_history
236
revid_set = set(revid_list)
239
def introduced_revisions(revid):
241
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
243
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
244
r.add(self._merge_sort[i][1])
248
if bzrlib.revision.is_null(revid):
250
if introduced_revisions(revid) & revid_set:
286
def get_sequence(self, revid):
287
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
290
def get_revision_history(self):
291
return self._full_history
293
def get_revid_sequence(self, revid_list, revid):
295
given a list of revision ids, return the sequence # of this revid in
304
def get_revids_from(self, revid_list, revid):
306
given a list of revision ids, yield revisions in graph order,
307
starting from revid. the list can be None if you just want to travel
308
across all revisions.
311
if (revid_list is None) or (revid in revid_list):
313
if not self._revision_graph.has_key(revid):
252
315
parents = self._revision_graph[revid]
253
316
if len(parents) == 0:
255
318
revid = parents[0]
257
321
def get_short_revision_history_by_fileid(self, file_id):
322
# wow. is this really the only way we can get this list? by
323
# man-handling the weave store directly? :-0
258
324
# FIXME: would be awesome if we could get, for a folder, the list of
259
# revisions where items within that folder changed.i
261
# FIXME: Workaround for bzr versions prior to 1.6b3.
262
# Remove me eventually pretty please :)
263
w = self._branch.repository.weave_store.get_weave(
264
file_id, self._branch.repository.get_transaction())
265
w_revids = w.versions()
266
revids = [r for r in self._full_history if r in w_revids]
267
except AttributeError:
268
possible_keys = [(file_id, revid) for revid in self._full_history]
269
get_parent_map = self._branch.repository.texts.get_parent_map
270
# We chunk the requests as this works better with GraphIndex.
271
# See _filter_revisions_touching_file_id in bzrlib/log.py
272
# for more information.
275
for start in xrange(0, len(possible_keys), chunk_size):
276
next_keys = possible_keys[start:start + chunk_size]
277
revids += [k[1] for k in get_parent_map(next_keys)]
278
del possible_keys, next_keys
325
# revisions where items within that folder changed.
326
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
327
w_revids = w.versions()
328
revids = [r for r in self._full_history if r in w_revids]
281
332
def get_revision_history_since(self, revid_list, date):
282
333
# if a user asks for revisions starting at 01-sep, they mean inclusive,
283
334
# so start at midnight on 02-sep.
284
335
date = date + datetime.timedelta(days=1)
285
# our revid list is sorted in REVERSE date order,
286
# so go thru some hoops here...
336
# our revid list is sorted in REVERSE date order, so go thru some hoops here...
287
337
revid_list.reverse()
288
index = bisect.bisect(_RevListToTimestamps(revid_list,
289
self._branch.repository),
338
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
293
341
revid_list.reverse()
295
343
return revid_list[index:]
346
def get_revision_history_matching(self, revid_list, text):
347
self.log.debug('searching %d revisions for %r', len(revid_list), text)
349
# this is going to be painfully slow. :(
352
for revid in revid_list:
353
change = self.get_changes([ revid ])[0]
354
if text in change.comment.lower():
356
self.log.debug('searched %d revisions for %r in %r secs', len(revid_list), text, time.time() - z)
359
def get_revision_history_matching_indexed(self, revid_list, text):
360
self.log.debug('searching %d revisions for %r', len(revid_list), text)
362
if self._index is None:
363
return self.get_revision_history_matching(revid_list, text)
364
out = self._index.find(text, revid_list)
365
self.log.debug('searched %d revisions for %r in %r secs: %d results', len(revid_list), text, time.time() - z, len(out))
366
# put them in some coherent order :)
367
out = [r for r in self._full_history if r in out]
297
371
def get_search_revid_list(self, query, revid_list):
299
373
given a "quick-search" query, try a few obvious possible meanings:
301
375
- revision id or # ("128.1.3")
302
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
303
iso style "yyyy-mm-dd")
376
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
304
377
- comment text as a fallback
306
379
and return a revid list that matches.
357
425
if revid is None:
359
427
if revid == 'head:':
360
return self.last_revid
362
if self.revno_re.match(revid):
363
revid = self._revno_revid[revid]
365
raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
428
return self._last_revid
429
if self.revno_re.match(revid):
430
revid = self._revno_revid[revid]
368
434
def get_file_view(self, revid, file_id):
370
Given a revid and optional path, return a (revlist, revid) for
371
navigation through the current scope: from the revid (or the latest
372
revision) back to the original revision.
436
Given an optional revid and optional path, return a (revlist, revid)
437
for navigation through the current scope: from the revid (or the
438
latest revision) back to the original revision.
374
440
If file_id is None, the entire revision history is the list scope.
441
If revid is None, the latest revision is used.
376
443
if revid is None:
377
revid = self.last_revid
444
revid = self._last_revid
378
445
if file_id is not None:
379
# since revid is 'start_revid', possibly should start the path
380
# tracing from revid... FIXME
446
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
447
inv = self._branch.repository.get_revision_inventory(revid)
381
448
revlist = list(self.get_short_revision_history_by_fileid(file_id))
382
449
revlist = list(self.get_revids_from(revlist, revid))
384
451
revlist = list(self.get_revids_from(None, revid))
454
return revlist, revid
387
457
def get_view(self, revid, start_revid, file_id, query=None):
389
459
use the URL parameters (revid, start_revid, file_id, and query) to
390
460
determine the revision list we're viewing (start_revid, file_id, query)
391
461
and where we are in it (revid).
393
- if a query is given, we're viewing query results.
394
- if a file_id is given, we're viewing revisions for a specific
396
- if a start_revid is given, we're viewing the branch from a
397
specific revision up the tree.
399
these may be combined to view revisions for a specific file, from
400
a specific revision, with a specific search query.
402
returns a new (revid, start_revid, revid_list) where:
463
if a query is given, we're viewing query results.
464
if a file_id is given, we're viewing revisions for a specific file.
465
if a start_revid is given, we're viewing the branch from a
466
specific revision up the tree.
467
(these may be combined to view revisions for a specific file, from
468
a specific revision, with a specific search query.)
470
returns a new (revid, start_revid, revid_list, scan_list) where:
404
472
- revid: current position within the view
405
473
- start_revid: starting revision of this view
406
474
- revid_list: list of revision ids for this view
408
476
file_id and query are never changed so aren't returned, but they may
409
477
contain vital context for future url navigation.
411
if start_revid is None:
412
start_revid = self.last_revid
414
479
if query is None:
415
revid_list = self.get_file_view(start_revid, file_id)
480
revid_list, start_revid = self.get_file_view(start_revid, file_id)
416
481
if revid is None:
417
482
revid = start_revid
418
483
if revid not in revid_list:
419
484
# if the given revid is not in the revlist, use a revlist that
420
485
# starts at the given revid.
421
revid_list = self.get_file_view(revid, file_id)
486
revid_list, start_revid = self.get_file_view(revid, file_id)
423
487
return revid, start_revid, revid_list
425
489
# potentially limit the search
426
if file_id is not None:
427
revid_list = self.get_file_view(start_revid, file_id)
490
if (start_revid is not None) or (file_id is not None):
491
revid_list, start_revid = self.get_file_view(start_revid, file_id)
429
493
revid_list = None
430
revid_list = search.search_revisions(self._branch, query)
431
if revid_list and len(revid_list) > 0:
495
revid_list = self.get_search_revid_list(query, revid_list)
496
if len(revid_list) > 0:
432
497
if revid not in revid_list:
433
498
revid = revid_list[0]
434
499
return revid, start_revid, revid_list
436
# XXX: This should return a message saying that the search could
437
# not be completed due to either missing the plugin or missing a
439
502
return None, None, []
441
505
def get_inventory(self, revid):
442
if revid not in self._inventory_cache:
443
self._inventory_cache[revid] = (
444
self._branch.repository.get_revision_inventory(revid))
445
return self._inventory_cache[revid]
506
return self._branch.repository.get_revision_inventory(revid)
447
509
def get_path(self, revid, file_id):
448
510
if (file_id is None) or (file_id == ''):
450
path = self.get_inventory(revid).id2path(file_id)
512
path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
451
513
if (len(path) > 0) and not path.startswith('/'):
452
514
path = '/' + path
455
518
def get_file_id(self, revid, path):
456
519
if (len(path) > 0) and not path.startswith('/'):
457
520
path = '/' + path
458
return self.get_inventory(revid).path2id(path)
521
return self._branch.repository.get_revision_inventory(revid).path2id(path)
523
def get_where_merged(self, revid):
525
return self._where_merged[revid]
460
529
def get_merge_point_list(self, revid):
462
531
Return the list of revids that have merged this node.
464
if '.' not in self.get_revno(revid):
533
if revid in self._history:
469
children = self._where_merged.get(revid, [])
538
children = self.get_where_merged(revid)
471
540
for child in children:
472
541
child_parents = self._revision_graph[child]
495
564
revnol = revno.split(".")
496
565
revnos = ".".join(revnol[:-2])
497
566
revnolast = int(revnol[-1])
498
if revnos in d.keys():
567
if d.has_key(revnos):
500
569
if revnolast < m:
501
d[revnos] = (revnolast, revid)
570
d[revnos] = ( revnolast, revid )
503
d[revnos] = (revnolast, revid)
505
return [d[revnos][1] for revnos in d.keys()]
507
def add_branch_nicks(self, change):
572
d[revnos] = ( revnolast, revid )
574
return [ d[revnos][1] for revnos in d.keys() ]
576
def get_branch_nicks(self, changes):
509
given a 'change', fill in the branch nicks on all parents and merge
578
given a list of changes from L{get_changes}, fill in the branch nicks
579
on all parents and merge points.
512
581
fetch_set = set()
513
for p in change.parents:
514
fetch_set.add(p.revid)
515
for p in change.merge_points:
516
fetch_set.add(p.revid)
582
for change in changes:
583
for p in change.parents:
584
fetch_set.add(p.revid)
585
for p in change.merge_points:
586
fetch_set.add(p.revid)
517
587
p_changes = self.get_changes(list(fetch_set))
518
588
p_change_dict = dict([(c.revid, c) for c in p_changes])
519
for p in change.parents:
520
if p.revid in p_change_dict:
521
p.branch_nick = p_change_dict[p.revid].branch_nick
523
p.branch_nick = '(missing)'
524
for p in change.merge_points:
525
if p.revid in p_change_dict:
526
p.branch_nick = p_change_dict[p.revid].branch_nick
528
p.branch_nick = '(missing)'
530
def get_changes(self, revid_list):
531
"""Return a list of changes objects for the given revids.
533
Revisions not present and NULL_REVISION will be ignored.
535
changes = self.get_changes_uncached(revid_list)
536
if len(changes) == 0:
589
for change in changes:
590
# arch-converted branches may not have merged branch info :(
591
for p in change.parents:
592
if p.revid in p_change_dict:
593
p.branch_nick = p_change_dict[p.revid].branch_nick
595
p.branch_nick = '(missing)'
596
for p in change.merge_points:
597
if p.revid in p_change_dict:
598
p.branch_nick = p_change_dict[p.revid].branch_nick
600
p.branch_nick = '(missing)'
603
def get_changes(self, revid_list, get_diffs=False):
604
if self._change_cache is None:
605
changes = self.get_changes_uncached(revid_list, get_diffs)
607
changes = self._change_cache.get_changes(revid_list, get_diffs)
539
611
# some data needs to be recalculated each time, because it may
540
612
# change as new revisions are added.
541
for change in changes:
542
merge_revids = self.simplify_merge_point_list(
543
self.get_merge_point_list(change.revid))
544
change.merge_points = [
545
util.Container(revid=r,
546
revno=self.get_revno(r)) for r in merge_revids]
547
if len(change.parents) > 0:
548
change.parents = [util.Container(revid=r,
549
revno=self.get_revno(r)) for r in change.parents]
550
change.revno = self.get_revno(change.revid)
553
for change in changes:
554
change.parity = parity
613
for i in xrange(len(revid_list)):
614
revid = revid_list[i]
616
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
617
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
559
def get_changes_uncached(self, revid_list):
560
# FIXME: deprecated method in getting a null revision
561
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
563
parent_map = self._branch.repository.get_graph().get_parent_map(
565
# We need to return the answer in the same order as the input,
567
present_revids = [revid for revid in revid_list
568
if revid in parent_map]
569
rev_list = self._branch.repository.get_revisions(present_revids)
571
return [self._change_from_revision(rev) for rev in rev_list]
573
def _change_from_revision(self, revision):
575
Given a bzrlib Revision, return a processed "change" for use in
621
# alright, let's profile this sucka.
622
def _get_changes_profiled(self, revid_list, get_diffs=False):
623
from loggerhead.lsprof import profile
625
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
628
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
629
self.log.info('lsprof complete!')
632
def _get_deltas_for_revisions_with_trees(self, revisions):
633
"""Produce a generator of revision deltas.
635
Note that the input is a sequence of REVISIONS, not revision_ids.
636
Trees will be held in memory until the generator exits.
637
Each delta is relative to the revision's lefthand predecessor.
639
required_trees = set()
640
for revision in revisions:
641
required_trees.add(revision.revision_id)
642
required_trees.update(revision.parent_ids[:1])
643
trees = dict((t.get_revision_id(), t) for
644
t in self._branch.repository.revision_trees(required_trees))
646
self._branch.repository.lock_read()
648
for revision in revisions:
649
if not revision.parent_ids:
650
old_tree = self._branch.repository.revision_tree(None)
652
old_tree = trees[revision.parent_ids[0]]
653
tree = trees[revision.revision_id]
654
ret.append((tree, old_tree, tree.changes_from(old_tree)))
657
self._branch.repository.unlock()
659
def entry_from_revision(self, revision):
578
660
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
580
parents = [util.Container(revid=r,
581
revno=self.get_revno(r)) for r in revision.parent_ids]
662
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
664
if len(parents) == 0:
667
left_parent = revision.parent_ids[0]
583
669
message, short_message = clean_message(revision.message)
586
authors = revision.get_apparent_authors()
587
except AttributeError:
588
authors = [revision.get_apparent_author()]
591
672
'revid': revision.revision_id,
673
'revno': self.get_revno(revision.revision_id),
592
674
'date': commit_time,
675
'author': revision.committer,
594
676
'branch_nick': revision.properties.get('branch-nick', None),
595
677
'short_comment': short_message,
596
678
'comment': revision.message,
597
679
'comment_clean': [util.html_clean(s) for s in message],
598
'parents': revision.parent_ids,
600
682
return util.Container(entry)
602
def get_file_changes_uncached(self, entry):
603
repo = self._branch.repository
605
old_revid = entry.parents[0].revid
607
old_revid = bzrlib.revision.NULL_REVISION
608
return self.file_changes_for_revision_ids(old_revid, entry.revid)
610
def get_file_changes(self, entry):
611
if self._file_change_cache is None:
612
return self.get_file_changes_uncached(entry)
614
return self._file_change_cache.get_file_changes(entry)
616
def add_changes(self, entry):
617
changes = self.get_file_changes(entry)
618
entry.changes = changes
685
@with_bzrlib_read_lock
686
def get_changes_uncached(self, revid_list, get_diffs=False):
690
rev_list = self._branch.repository.get_revisions(revid_list)
692
except (KeyError, bzrlib.errors.NoSuchRevision), e:
693
# this sometimes happens with arch-converted branches.
694
# i don't know why. :(
695
self.log.debug('No such revision (skipping): %s', e)
696
revid_list.remove(e.revision)
698
delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
699
combined_list = zip(rev_list, delta_list)
702
for rev, (new_tree, old_tree, delta) in combined_list:
703
entry = self.entry_from_revision(rev)
704
entry.changes = self.parse_delta(delta, get_diffs, old_tree, new_tree)
705
entries.append(entry)
709
@with_bzrlib_read_lock
710
def _get_diff(self, revid1, revid2):
711
rev_tree1 = self._branch.repository.revision_tree(revid1)
712
rev_tree2 = self._branch.repository.revision_tree(revid2)
713
delta = rev_tree2.changes_from(rev_tree1)
714
return rev_tree1, rev_tree2, delta
716
def get_diff(self, revid1, revid2):
717
rev_tree1, rev_tree2, delta = self._get_diff(revid1, revid2)
718
entry = self.get_changes([ revid2 ], False)[0]
719
entry.changes = self.parse_delta(delta, True, rev_tree1, rev_tree2)
620
723
def get_file(self, file_id, revid):
621
724
"returns (path, filename, data)"
622
725
inv = self.get_inventory(revid)
626
729
if not path.startswith('/'):
627
730
path = '/' + path
628
731
return path, inv_entry.name, rev_tree.get_file_text(file_id)
630
def file_changes_for_revision_ids(self, old_revid, new_revid):
734
def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
632
736
Return a nested data structure containing the changes in a delta::
634
738
added: list((filename, file_id)),
635
739
renamed: list((old_filename, new_filename, file_id)),
636
740
deleted: list((filename, file_id)),
641
text_changes: list((filename, file_id)),
643
repo = self._branch.repository
644
if bzrlib.revision.is_null(old_revid) or \
645
bzrlib.revision.is_null(new_revid):
646
old_tree, new_tree = map(
647
repo.revision_tree, [old_revid, new_revid])
649
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
651
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
653
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
655
return util.Container(
656
added=sorted(reporter.added, key=lambda x:x.filename),
657
renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
658
removed=sorted(reporter.removed, key=lambda x:x.filename),
659
modified=sorted(reporter.modified, key=lambda x:x.filename),
660
text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))
748
type: str('context', 'delete', or 'insert'),
754
if C{get_diffs} is false, the C{chunks} will be omitted.
761
def rich_filename(path, kind):
762
if kind == 'directory':
764
if kind == 'symlink':
768
def process_diff(diff):
771
for line in diff.splitlines():
774
if line.startswith('+++ ') or line.startswith('--- '):
776
if line.startswith('@@ '):
778
if chunk is not None:
780
chunk = util.Container()
782
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
783
old_lineno = lines[0]
784
new_lineno = lines[1]
785
elif line.startswith(' '):
786
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
787
type='context', line=util.html_clean(line[1:])))
790
elif line.startswith('+'):
791
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
792
type='insert', line=util.html_clean(line[1:])))
794
elif line.startswith('-'):
795
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
796
type='delete', line=util.html_clean(line[1:])))
799
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
800
type='unknown', line=util.html_clean(repr(line))))
801
if chunk is not None:
805
def handle_modify(old_path, new_path, fid, kind):
807
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
809
old_lines = old_tree.get_file_lines(fid)
810
new_lines = new_tree.get_file_lines(fid)
813
bzrlib.diff.internal_diff(old_path, old_lines,
814
new_path, new_lines, buffer)
815
except bzrlib.errors.BinaryFile:
818
diff = buffer.getvalue()
819
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
821
for path, fid, kind in delta.added:
822
added.append((rich_filename(path, kind), fid))
824
for path, fid, kind, text_modified, meta_modified in delta.modified:
825
handle_modify(path, path, fid, kind)
827
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
828
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
829
if meta_modified or text_modified:
830
handle_modify(oldpath, newpath, fid, kind)
832
for path, fid, kind in delta.removed:
833
removed.append((rich_filename(path, kind), fid))
835
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
838
def add_side_by_side(changes):
839
# FIXME: this is a rotten API.
840
for change in changes:
841
for m in change.changes.modified:
842
m.sbs_chunks = _make_side_by_side(m.chunks)
845
def get_filelist(self, inv, path, sort_type=None):
847
return the list of all files (and their attributes) within a given
850
while path.endswith('/'):
852
if path.startswith('/'):
855
entries = inv.entries()
858
for filepath, entry in entries:
859
if posixpath.dirname(filepath) != path:
861
filename = posixpath.basename(filepath)
862
rich_filename = filename
864
if entry.kind == 'directory':
867
revid = entry.revision
868
revision = self._branch.repository.get_revision(revid)
870
change = util.Container(date=datetime.datetime.fromtimestamp(revision.timestamp),
871
revno=self.get_revno(revid))
873
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
874
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
875
file_list.append(file)
877
if sort_type == 'filename':
878
file_list.sort(key=lambda x: x.filename)
879
elif sort_type == 'size':
880
file_list.sort(key=lambda x: x.size)
881
elif sort_type == 'date':
882
file_list.sort(key=lambda x: x.change.date)
885
for file in file_list:
892
_BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
895
def annotate_file(self, file_id, revid):
900
file_revid = self.get_inventory(revid)[file_id].revision
903
# because we cache revision metadata ourselves, it's actually much
904
# faster to call 'annotate_iter' on the weave directly than it is to
905
# ask bzrlib to annotate for us.
906
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
909
for line_revid, text in w.annotate_iter(file_revid):
910
revid_set.add(line_revid)
911
if self._BADCHARS_RE.match(text):
912
# bail out; this isn't displayable text
913
yield util.Container(parity=0, lineno=1, status='same',
914
text='<i>' + util.html_clean('(This is a binary file.)') + '</i>',
915
change=util.Container())
917
change_cache = dict([(c.revid, c) for c in self.get_changes(list(revid_set))])
919
last_line_revid = None
920
for line_revid, text in w.annotate_iter(file_revid):
921
if line_revid == last_line_revid:
922
# remember which lines have a new revno and which don't
927
last_line_revid = line_revid
928
change = change_cache[line_revid]
929
trunc_revno = change.revno
930
if len(trunc_revno) > 10:
931
trunc_revno = trunc_revno[:9] + '...'
933
yield util.Container(parity=parity, lineno=lineno, status=status,
934
change=change, text=util.html_clean(text))
937
self.log.debug('annotate: %r secs' % (time.time() - z,))
940
@with_bzrlib_read_lock
941
def get_bundle(self, revid, compare_revid=None):
942
if compare_revid is None:
943
parents = self._revision_graph[revid]
945
compare_revid = parents[0]
949
bzrlib.bundle.serializer.write_bundle(self._branch.repository, revid, compare_revid, s)