86
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
125
def is_branch(folder):
127
bzrlib.branch.Branch.open(folder)
133
def clean_message(message):
134
"""Clean up a commit message and return it and a short (1-line) version.
136
Commit messages that are long single lines are reflowed using the textwrap
137
module (Robey, the original author of this code, apparently favored this
140
message = message.splitlines()
142
if len(message) == 1:
143
message = textwrap.wrap(message[0])
145
if len(message) == 0:
146
# We can end up where when (a) the commit message was empty or (b)
147
# when the message consisted entirely of whitespace, in which case
148
# textwrap.wrap() returns an empty list.
151
# Make short form of commit message.
152
short_message = message[0]
153
if len(short_message) > 80:
154
short_message = short_message[:80] + '...'
156
return message, short_message
159
def rich_filename(path, kind):
160
if kind == 'directory':
162
if kind == 'symlink':
169
class _RevListToTimestamps(object):
170
"""This takes a list of revisions, and allows you to bisect by date"""
172
__slots__ = ['revid_list', 'repository']
174
def __init__(self, revid_list, repository):
175
self.revid_list = revid_list
176
self.repository = repository
178
def __getitem__(self, index):
179
"""Get the date of the index'd item"""
180
return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
183
return len(self.revid_list)
89
186
class History (object):
91
188
def __init__(self):
92
189
self._change_cache = None
93
self._cache_lock = threading.Lock()
190
self._file_change_cache = None
94
192
self._lock = threading.RLock()
97
if self._change_cache is not None:
98
self._change_cache.close()
99
self._change_cache_diffs.close()
100
self._change_cache = None
101
self._change_cache_diffs = None
104
def from_branch(cls, branch):
195
def from_branch(cls, branch, name=None):
107
198
self._branch = branch
108
self._history = branch.revision_history()
109
self._revision_graph = branch.repository.get_revision_graph()
110
self._last_revid = self._history[-1]
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,))
112
210
self._full_history = []
113
211
self._revision_info = {}
114
212
self._revno_revid = {}
115
213
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
117
214
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
118
215
self._full_history.append(revid)
119
216
revno_str = '.'.join(str(n) for n in revno)
120
217
self._revno_revid[revno_str] = revid
121
218
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
125
220
# cache merge info
126
221
self._where_merged = {}
127
222
for revid in self._revision_graph.keys():
128
if not revid in self._full_history:
223
if not revid in self._full_history:
130
225
for parent in self._revision_graph[revid]:
131
226
self._where_merged.setdefault(parent, set()).add(revid)
133
log.info('built revision graph cache: %r secs' % (time.time() - z,))
228
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
137
def from_folder(cls, path):
232
def from_folder(cls, path, name=None):
138
233
b = bzrlib.branch.Branch.open(path)
139
return cls.from_branch(b)
234
return cls.from_branch(b, name)
141
236
@with_branch_lock
142
237
def out_of_date(self):
143
if self._branch.revision_history()[-1] != self._last_revid:
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__:
148
def use_cache(self, path):
149
if not os.path.exists(path):
151
# keep a separate cache for the diffs, because they're very time-consuming to fetch.
152
cachefile = os.path.join(path, 'changes')
153
cachefile_diffs = os.path.join(path, 'changes-diffs')
155
# why can't shelve allow 'cw'?
156
if not os.path.exists(cachefile):
157
self._change_cache = shelve.open(cachefile, 'c', protocol=2)
159
self._change_cache = shelve.open(cachefile, 'w', protocol=2)
160
if not os.path.exists(cachefile_diffs):
161
self._change_cache_diffs = shelve.open(cachefile_diffs, 'c', protocol=2)
163
self._change_cache_diffs = shelve.open(cachefile_diffs, 'w', protocol=2)
165
# once we process a change (revision), it should be the same forever.
166
log.info('Using change cache %s; %d, %d entries.' % (path, len(self._change_cache), len(self._change_cache_diffs)))
167
self._change_cache_filename = cachefile
168
self._change_cache_diffs_filename = cachefile_diffs
171
def dont_use_cache(self):
172
# called when a new history object needs to be created. we can't use
173
# the cache files anymore; they belong to the new history object.
174
if self._change_cache is None:
176
self._change_cache.close()
177
self._change_cache_diffs.close()
178
self._change_cache = None
179
self._change_cache_diffs = None
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:
182
267
def flush_cache(self):
183
268
if self._change_cache is None:
185
self._change_cache.sync()
186
self._change_cache_diffs.sync()
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()
188
278
last_revid = property(lambda self: self._last_revid, None, None)
190
count = property(lambda self: self._count, None, None)
192
280
@with_branch_lock
193
def get_revision(self, revid):
194
return self._branch.repository.get_revision(revid)
281
def get_config(self):
282
return self._branch.get_config()
196
284
def get_revno(self, revid):
197
285
if revid not in self._revision_info:
245
318
revids = [r for r in self._full_history if r in w_revids]
322
def get_revision_history_since(self, revid_list, date):
323
# if a user asks for revisions starting at 01-sep, they mean inclusive,
324
# so start at midnight on 02-sep.
325
date = date + datetime.timedelta(days=1)
326
# our revid list is sorted in REVERSE date order, so go thru some hoops here...
328
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
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]
361
def get_search_revid_list(self, query, revid_list):
363
given a "quick-search" query, try a few obvious possible meanings:
365
- revision id or # ("128.1.3")
366
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
367
- comment text as a fallback
369
and return a revid list that matches.
371
# FIXME: there is some silliness in this action. we have to look up
372
# all the relevant changes (time-consuming) only to return a list of
373
# revids which will be used to fetch a set of changes again.
375
# if they entered a revid, just jump straight there; ignore the passed-in revid_list
376
revid = self.fix_revid(query)
377
if revid is not None:
378
if isinstance(revid, unicode):
379
revid = revid.encode('utf-8')
380
changes = self.get_changes([ revid ])
381
if (changes is not None) and (len(changes) > 0):
385
m = self.us_date_re.match(query)
387
date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
389
m = self.earth_date_re.match(query)
391
date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
393
m = self.iso_date_re.match(query)
395
date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
397
if revid_list is None:
398
# if no limit to the query was given, search only the direct-parent path.
399
revid_list = list(self.get_revids_from(None, self._last_revid))
400
return self.get_revision_history_since(revid_list, date)
402
# check comment fields.
403
if revid_list is None:
404
revid_list = self._full_history
405
return self.get_revision_history_matching_indexed(revid_list, query)
248
407
revno_re = re.compile(r'^[\d\.]+$')
408
# the date regex are without a final '$' so that queries like
409
# "2006-11-30 12:15" still mostly work. (i think it's better to give
410
# them 90% of what they want instead of nothing at all.)
411
us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
412
earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
413
iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
250
415
def fix_revid(self, revid):
251
416
# if a "revid" is actually a dotted revno, convert it to a revid
252
417
if revid is None:
420
return self._last_revid
254
421
if self.revno_re.match(revid):
255
422
revid = self._revno_revid[revid]
258
425
@with_branch_lock
259
def get_navigation(self, revid, path):
426
def get_file_view(self, revid, file_id):
261
Given an optional revid and optional path, return a (revlist, revid)
262
for navigation through the current scope: from the revid (or the
263
latest revision) back to the original revision.
265
If path is None, the entire revision history is the list scope.
266
If revid is None, the latest revision is used.
428
Given a revid and optional path, return a (revlist, revid) for
429
navigation through the current scope: from the revid (or the latest
430
revision) back to the original revision.
432
If file_id is None, the entire revision history is the list scope.
268
434
if revid is None:
269
435
revid = self._last_revid
271
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
272
inv = self._branch.repository.get_revision_inventory(revid)
273
revlist = list(self.get_short_revision_history_by_fileid(inv.path2id(path)))
436
if file_id is not None:
437
# since revid is 'start_revid', possibly should start the path
438
# tracing from revid... FIXME
439
revlist = list(self.get_short_revision_history_by_fileid(file_id))
274
440
revlist = list(self.get_revids_from(revlist, revid))
276
442
revlist = list(self.get_revids_from(None, revid))
279
return revlist, revid
446
def get_view(self, revid, start_revid, file_id, query=None):
448
use the URL parameters (revid, start_revid, file_id, and query) to
449
determine the revision list we're viewing (start_revid, file_id, query)
450
and where we are in it (revid).
452
if a query is given, we're viewing query results.
453
if a file_id is given, we're viewing revisions for a specific file.
454
if a start_revid is given, we're viewing the branch from a
455
specific revision up the tree.
456
(these may be combined to view revisions for a specific file, from
457
a specific revision, with a specific search query.)
459
returns a new (revid, start_revid, revid_list, scan_list) where:
461
- revid: current position within the view
462
- start_revid: starting revision of this view
463
- revid_list: list of revision ids for this view
465
file_id and query are never changed so aren't returned, but they may
466
contain vital context for future url navigation.
468
if start_revid is None:
469
start_revid = self._last_revid
472
revid_list = self.get_file_view(start_revid, file_id)
475
if revid not in revid_list:
476
# if the given revid is not in the revlist, use a revlist that
477
# starts at the given revid.
478
revid_list= self.get_file_view(revid, file_id)
480
return revid, start_revid, revid_list
482
# potentially limit the search
483
if file_id is not None:
484
revid_list = self.get_file_view(start_revid, file_id)
488
revid_list = self.get_search_revid_list(query, revid_list)
489
if len(revid_list) > 0:
490
if revid not in revid_list:
491
revid = revid_list[0]
492
return revid, start_revid, revid_list
495
return None, None, []
281
497
@with_branch_lock
282
498
def get_inventory(self, revid):
283
499
return self._branch.repository.get_revision_inventory(revid)
285
def get_where_merged(self, revid):
287
return self._where_merged[revid]
502
def get_path(self, revid, file_id):
503
if (file_id is None) or (file_id == ''):
505
path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
506
if (len(path) > 0) and not path.startswith('/'):
511
def get_file_id(self, revid, path):
512
if (len(path) > 0) and not path.startswith('/'):
514
return self._branch.repository.get_revision_inventory(revid).path2id(path)
291
517
def get_merge_point_list(self, revid):
293
519
Return the list of revids that have merged this node.
295
if revid in self._history:
521
if '.' not in self.get_revno(revid):
300
children = self.get_where_merged(revid)
526
children = self._where_merged.get(revid, [])
302
528
for child in children:
303
529
child_parents = self._revision_graph[child]
334
560
d[revnos] = ( revnolast, revid )
336
562
return [ d[revnos][1] for revnos in d.keys() ]
338
def get_changelist(self, revid_list):
339
for revid in revid_list:
340
yield self.get_change(revid)
564
def get_branch_nicks(self, changes):
566
given a list of changes from L{get_changes}, fill in the branch nicks
567
on all parents and merge points.
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)
575
p_changes = self.get_changes(list(fetch_set))
576
p_change_dict = dict([(c.revid, c) for c in p_changes])
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)'
342
590
@with_branch_lock
343
def get_change(self, revid, get_diffs=False):
591
def get_changes(self, revid_list):
344
592
if self._change_cache is None:
345
return self._get_change(revid, get_diffs)
347
# if the revid is in unicode, use the utf-8 encoding as the key
349
if isinstance(revid, unicode):
350
srevid = revid.encode('utf-8')
351
return self._get_change_from_cache(revid, srevid, get_diffs)
354
def _get_change_from_cache(self, revid, srevid, get_diffs):
356
cache = self._change_cache_diffs
358
cache = self._change_cache
363
if get_diffs and (srevid in self._change_cache):
364
# salvage the non-diff entry for a jump-start
365
c = self._change_cache[srevid]
366
if len(c.parents) == 0:
369
left_parent = c.parents[0].revid
370
c.changes = self.diff_revisions(revid, left_parent, get_diffs=True)
373
#log.debug('Entry cache miss: %r' % (revid,))
374
c = self._get_change(revid, get_diffs=get_diffs)
593
changes = self.get_changes_uncached(revid_list)
595
changes = self._change_cache.get_changes(revid_list)
596
if len(changes) == 0:
377
599
# some data needs to be recalculated each time, because it may
378
600
# change as new revisions are added.
379
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
380
c.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
601
for change in changes:
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]
604
change.revno = self.get_revno(change.revid)
607
for change in changes:
608
change.parity = parity
384
613
# alright, let's profile this sucka.
385
def _get_change_profiled(self, revid, get_diffs=False):
614
def _get_changes_profiled(self, revid_list, get_diffs=False):
386
615
from loggerhead.lsprof import profile
388
ret, stats = profile(self._get_change, revid, get_diffs)
617
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
391
620
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
621
self.log.info('lsprof complete!')
394
def _get_change(self, revid, get_diffs=False):
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()
396
rev = self._branch.repository.get_revision(revid)
397
except (KeyError, bzrlib.errors.NoSuchRevision):
402
'date': datetime.datetime.fromtimestamp(0),
405
'short_comment': 'missing',
406
'comment': 'missing',
407
'comment_clean': 'missing',
412
log.error('ghost entry: %r' % (revid,))
413
return util.Container(entry)
415
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
417
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
419
if len(parents) == 0:
422
left_parent = rev.parent_ids[0]
424
message = rev.message.splitlines()
425
if len(message) == 1:
426
# robey-style 1-line long message
427
message = textwrap.wrap(message[0])
429
# make short form of commit message
430
short_message = message[0]
431
if len(short_message) > 60:
432
short_message = short_message[:60] + '...'
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]
657
message, short_message = clean_message(revision.message)
436
'revno': self.get_revno(revid),
660
'revid': revision.revision_id,
437
661
'date': commit_time,
438
'author': rev.committer,
439
'branch_nick': rev.properties.get('branch-nick', None),
662
'author': revision.committer,
663
'branch_nick': revision.properties.get('branch-nick', None),
440
664
'short_comment': short_message,
441
'comment': rev.message,
665
'comment': revision.message,
442
666
'comment_clean': [util.html_clean(s) for s in message],
443
667
'parents': parents,
444
'changes': self.diff_revisions(revid, left_parent, get_diffs=get_diffs),
446
669
return util.Container(entry)
448
def scan_range(self, revlist, revid, pagesize=20):
450
yield a list of (label, title, revid) for a scan range through the full
451
branch history, centered around the given revid.
453
example: [ ('<<', 'Previous page', 'rrr'), ('-10', 'Forward 10', 'rrr'),
454
('*', None, None), ('+10', 'Back 10', 'rrr'),
455
('+30', 'Back 30', 'rrr'), ('>>', 'Next page', 'rrr') ]
457
next/prev page are always using the pagesize.
460
pos = self.get_revid_sequence(revlist, revid)
463
yield (u'\xab', 'Previous page', revlist[max(0, pos - pagesize)])
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):
699
if self._file_change_cache is None:
700
return self.get_file_changes_uncached(entries)
465
yield (u'\xab', None, None)
468
for offset in util.scan_range(pos, count, pagesize):
469
if (offset > 0) and (offset_sign < 0):
471
# show current position
472
# yield ('[%s]' % (self.get_revno(revlist[pos]),), None, None)
473
# yield (u'\u2022', None, None)
474
yield (u'\u00b7', None, None)
476
title = 'Back %d' % (-offset,)
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
478
title = 'Forward %d' % (offset,)
479
yield ('%+d' % (offset,), title, revlist[pos + offset])
482
yield (u'\xbb', 'Next page', revlist[min(count - 1, pos + pagesize)])
484
yield (u'\xbb', None, None)
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)
486
730
@with_branch_lock
487
def diff_revisions(self, revid, otherrevid, get_diffs=True):
731
def get_file(self, file_id, revid):
732
"returns (path, filename, data)"
733
inv = self.get_inventory(revid)
734
inv_entry = inv[file_id]
735
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
736
path = inv.id2path(file_id)
737
if not path.startswith('/'):
739
return path, inv_entry.name, rev_tree.get_file_text(file_id)
741
def _parse_diffs(self, old_tree, new_tree, delta):
489
Return a nested data structure containing the changes between two
492
added: list(filename),
493
renamed: list((old_filename, new_filename)),
494
deleted: list(filename),
743
Return a list of processed diffs, in the format::
507
if C{get_diffs} is false, the C{chunks} will be omitted.
510
new_tree = self._branch.repository.revision_tree(revid)
511
old_tree = self._branch.repository.revision_tree(otherrevid)
512
delta = new_tree.changes_from(old_tree)
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):
825
Return a nested data structure containing the changes in a delta::
827
added: list((filename, file_id)),
828
renamed: list((old_filename, new_filename, file_id)),
829
deleted: list((filename, file_id)),
519
def rich_filename(path, kind):
520
if kind == 'directory':
522
if kind == 'symlink':
526
def tree_lines(tree, fid):
529
tree_file = bzrlib.textfile.text_file(tree.get_file(fid))
530
return tree_file.readlines()
532
def process_diff(diff):
535
for line in diff.splitlines():
538
if line.startswith('+++ ') or line.startswith('--- '):
540
if line.startswith('@@ '):
542
if chunk is not None:
544
chunk = util.Container()
546
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
547
old_lineno = lines[0]
548
new_lineno = lines[1]
549
elif line.startswith(' '):
550
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
551
type='context', line=util.html_clean(line[1:])))
554
elif line.startswith('+'):
555
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
556
type='insert', line=util.html_clean(line[1:])))
558
elif line.startswith('-'):
559
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
560
type='delete', line=util.html_clean(line[1:])))
563
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
564
type='unknown', line=util.html_clean(repr(line))))
565
if chunk is not None:
569
def handle_modify(old_path, new_path, fid, kind):
571
modified.append(util.Container(filename=rich_filename(new_path, kind)))
573
old_lines = tree_lines(old_tree, fid)
574
new_lines = tree_lines(new_tree, fid)
576
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
577
diff = buffer.getvalue()
578
modified.append(util.Container(filename=rich_filename(new_path, kind), chunks=process_diff(diff)))
580
840
for path, fid, kind in delta.added:
581
added.append(rich_filename(path, kind))
841
added.append((rich_filename(path, kind), fid))
583
843
for path, fid, kind, text_modified, meta_modified in delta.modified:
584
handle_modify(path, path, fid, kind)
586
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
587
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind)))
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))
588
848
if meta_modified or text_modified:
589
handle_modify(oldpath, newpath, fid, kind)
849
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
591
851
for path, fid, kind in delta.removed:
592
removed.append(rich_filename(path, kind))
852
removed.append((rich_filename(path, kind), fid))
594
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)
596
863
@with_branch_lock
597
def get_filelist(self, inv, path):
864
def get_filelist(self, inv, file_id, sort_type=None):
599
866
return the list of all files (and their attributes) within a given
602
while path.endswith('/'):
604
if path.startswith('/'):
607
for filepath, entry in inv.entries():
608
if posixpath.dirname(filepath) != path:
610
filename = posixpath.basename(filepath)
611
rich_filename = filename
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():
612
884
pathname = filename
613
885
if entry.kind == 'directory':
617
888
revid = entry.revision
618
change = self.get_change(revid)
620
yield util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
621
pathname=pathname, revid=revid, change=change, parity=parity)
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]')
625
913
@with_branch_lock
626
914
def annotate_file(self, file_id, revid):
631
919
file_revid = self.get_inventory(revid)[file_id].revision
635
922
# because we cache revision metadata ourselves, it's actually much
636
923
# faster to call 'annotate_iter' on the weave directly than it is to
637
924
# ask bzrlib to annotate for us.
638
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))])
639
938
last_line_revid = None
640
939
for line_revid, text in w.annotate_iter(file_revid):
641
940
if line_revid == last_line_revid: