47
45
import bzrlib.branch
50
47
import bzrlib.errors
51
import bzrlib.lru_cache
52
48
import bzrlib.progress
53
49
import bzrlib.revision
54
import bzrlib.textfile
55
50
import bzrlib.tsort
58
53
# bzrlib's UIFactory is not thread-safe
59
54
uihack = threading.local()
62
56
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
64
57
def nested_progress_bar(self):
65
58
if getattr(uihack, '_progress_bar_stack', None) is None:
66
pbs = bzrlib.progress.ProgressBarStack(
67
klass=bzrlib.progress.DummyProgress)
68
uihack._progress_bar_stack = pbs
59
uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
69
60
return uihack._progress_bar_stack.get_nested()
71
62
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
65
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
66
while len(delete_list) < len(insert_list):
67
delete_list.append((None, '', 'context'))
68
while len(insert_list) < len(delete_list):
69
insert_list.append((None, '', 'context'))
70
while len(delete_list) > 0:
71
d = delete_list.pop(0)
72
i = insert_list.pop(0)
73
line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
74
old_line=d[1], new_line=i[1],
75
old_type=d[2], new_type=i[2]))
78
def _make_side_by_side(chunk_list):
80
turn a normal unified-style diff (post-processed by parse_delta) into a
81
side-by-side diff structure. the new structure is::
89
type: str('context' or 'changed'),
94
for chunk in chunk_list:
97
delete_list, insert_list = [], []
98
for line in chunk.diff:
99
# Add <wbr/> every X characters so we can wrap properly
100
wrap_line = re.findall(r'.{%d}|.+$' % 78, line.line)
101
wrap_lines = [util.html_clean(_line) for _line in wrap_line]
102
wrapped_line = wrap_char.join(wrap_lines)
104
if line.type == 'context':
105
if len(delete_list) or len(insert_list):
106
_process_side_by_side_buffers(line_list, delete_list,
108
delete_list, insert_list = [], []
109
line_list.append(util.Container(old_lineno=line.old_lineno,
110
new_lineno=line.new_lineno,
111
old_line=wrapped_line,
112
new_line=wrapped_line,
115
elif line.type == 'delete':
116
delete_list.append((line.old_lineno, wrapped_line, line.type))
117
elif line.type == 'insert':
118
insert_list.append((line.new_lineno, wrapped_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
73
125
def is_branch(folder):
75
127
bzrlib.branch.Branch.open(folder)
127
178
def __getitem__(self, index):
128
179
"""Get the date of the index'd item"""
129
return datetime.datetime.fromtimestamp(self.repository.get_revision(
130
self.revid_list[index]).timestamp)
180
return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
132
182
def __len__(self):
133
183
return len(self.revid_list)
135
class FileChangeReporter(object):
136
def __init__(self, old_inv, new_inv):
141
self.text_changes = []
142
self.old_inv = old_inv
143
self.new_inv = new_inv
145
def revid(self, inv, file_id):
147
return inv[file_id].revision
148
except bzrlib.errors.NoSuchId:
151
def report(self, file_id, paths, versioned, renamed, modified,
153
if modified not in ('unchanged', 'kind changed'):
154
if versioned == 'removed':
155
filename = rich_filename(paths[0], kind[0])
157
filename = rich_filename(paths[1], kind[1])
158
self.text_changes.append(util.Container(
159
filename=filename, file_id=file_id,
160
old_revision=self.revid(self.old_inv, file_id),
161
new_revision=self.revid(self.new_inv, file_id)))
162
if versioned == 'added':
163
self.added.append(util.Container(
164
filename=rich_filename(paths[1], kind),
165
file_id=file_id, kind=kind[1]))
166
elif versioned == 'removed':
167
self.removed.append(util.Container(
168
filename=rich_filename(paths[0], kind),
169
file_id=file_id, kind=kind[0]))
171
self.renamed.append(util.Container(
172
old_filename=rich_filename(paths[0], kind[0]),
173
new_filename=rich_filename(paths[1], kind[1]),
175
text_modified=modified == 'modified'))
177
self.modified.append(util.Container(
178
filename=rich_filename(paths[1], kind),
182
class RevInfoMemoryCache(object):
183
"""A store that validates values against the revids they were stored with.
185
We use a unique key for each branch.
187
The reason for not just using the revid as the key is so that when a new
188
value is provided for a branch, we replace the old value used for the
191
There is another implementation of the same interface in
192
loggerhead.changecache.RevInfoDiskCache.
195
def __init__(self, cache):
198
def get(self, key, revid):
199
"""Return the data associated with `key`, subject to a revid check.
201
If a value was stored under `key`, with the same revid, return it.
202
Otherwise return None.
204
cached = self._cache.get(key)
207
stored_revid, data = cached
208
if revid == stored_revid:
213
def set(self, key, revid, data):
214
"""Store `data` under `key`, to be checked against `revid` on get().
216
self._cache[key] = (revid, data)
219
186
class History (object):
220
187
"""Decorate a branch to provide information for rendering.
224
191
around it, serve the request, throw the History object away, unlock the
225
192
branch and throw it away.
227
:ivar _file_change_cache: An object that caches information about the
228
files that changed between two revisions.
229
:ivar _rev_info: A list of information about revisions. This is by far
230
the most cryptic data structure in loggerhead. At the top level, it
231
is a list of 3-tuples [(merge-info, where-merged, parents)].
232
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
233
like a merged sorted list, but the revno is stringified.
234
`where-merged` is a tuple of revisions that have this revision as a
235
non-lefthand parent. Finally, `parents` is just the usual list of
236
parents of this revision.
237
:ivar _rev_indices: A dictionary mapping each revision id to the index of
238
the information about it in _rev_info.
239
:ivar _full_history: A list of all revision ids in the ancestry of the
240
branch, in merge-sorted order. This is a bit silly, and shouldn't
241
really be stored on the instance...
242
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
194
:ivar _file_change_cache: xx
246
def _load_whole_history_data(self, caches, cache_key):
247
"""Set the attributes relating to the whole history of the branch.
249
:param caches: a list of caches with interfaces like
250
`RevInfoMemoryCache` and be ordered from fastest to slowest.
251
:param cache_key: the key to use with the caches.
253
self._rev_indices = None
254
self._rev_info = None
257
def update_missed_caches():
258
for cache in missed_caches:
259
cache.set(cache_key, self.last_revid, self._rev_info)
261
data = cache.get(cache_key, self.last_revid)
263
self._rev_info = data
264
update_missed_caches()
267
missed_caches.append(cache)
269
whole_history_data = compute_whole_history_data(self._branch)
270
self._rev_info, self._rev_indices = whole_history_data
271
update_missed_caches()
273
if self._rev_indices is not None:
274
self._full_history = []
275
self._revno_revid = {}
276
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
277
self._revno_revid[revno_str] = revid
278
self._full_history.append(revid)
280
self._full_history = []
281
self._revno_revid = {}
282
self._rev_indices = {}
283
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
284
self._rev_indices[revid] = seq
285
self._revno_revid[revno_str] = revid
286
self._full_history.append(revid)
288
def __init__(self, branch, whole_history_data_cache, file_cache=None,
289
revinfo_disk_cache=None, cache_key=None):
197
def __init__(self, branch, whole_history_data_cache):
290
198
assert branch.is_locked(), (
291
199
"Can only construct a History object with a read-locked branch.")
292
if file_cache is not None:
293
self._file_change_cache = file_cache
294
file_cache.history = self
296
self._file_change_cache = None
200
self._file_change_cache = None
297
201
self._branch = branch
298
202
self._inventory_cache = {}
299
self._branch_nick = self._branch.get_config().get_nickname()
300
self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
203
self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
302
205
self.last_revid = branch.last_revision()
304
caches = [RevInfoMemoryCache(whole_history_data_cache)]
305
if revinfo_disk_cache:
306
caches.append(revinfo_disk_cache)
307
self._load_whole_history_data(caches, cache_key)
207
whole_history_data = whole_history_data_cache.get(self.last_revid)
208
if whole_history_data is None:
209
whole_history_data = compute_whole_history_data(branch)
210
whole_history_data_cache[self.last_revid] = whole_history_data
212
(self._revision_graph, self._full_history, self._revision_info,
213
self._revno_revid, self._merge_sort, self._where_merged
214
) = whole_history_data
217
def use_file_cache(self, cache):
218
self._file_change_cache = cache
310
221
def has_revisions(self):
354
262
# FIXME: would be awesome if we could get, for a folder, the list of
355
263
# revisions where items within that folder changed.i
357
# FIXME: Workaround for bzr versions prior to 1.6b3.
265
# FIXME: Workaround for bzr versions prior to 1.6b3.
358
266
# Remove me eventually pretty please :)
359
w = self._branch.repository.weave_store.get_weave(
360
file_id, self._branch.repository.get_transaction())
361
w_revids = w.versions()
362
revids = [r for r in self._full_history if r in w_revids]
267
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
268
w_revids = w.versions()
269
revids = [r for r in self._full_history if r in w_revids]
363
270
except AttributeError:
364
271
possible_keys = [(file_id, revid) for revid in self._full_history]
365
get_parent_map = self._branch.repository.texts.get_parent_map
366
# We chunk the requests as this works better with GraphIndex.
367
# See _filter_revisions_touching_file_id in bzrlib/log.py
368
# for more information.
371
for start in xrange(0, len(possible_keys), chunk_size):
372
next_keys = possible_keys[start:start + chunk_size]
373
revids += [k[1] for k in get_parent_map(next_keys)]
374
del possible_keys, next_keys
272
existing_keys = self._branch.repository.texts.get_parent_map(possible_keys)
273
revids = [revid for _, revid in existing_keys.iterkeys()]
377
276
def get_revision_history_since(self, revid_list, date):
378
277
# if a user asks for revisions starting at 01-sep, they mean inclusive,
379
278
# so start at midnight on 02-sep.
380
279
date = date + datetime.timedelta(days=1)
381
# our revid list is sorted in REVERSE date order,
382
# so go thru some hoops here...
280
# our revid list is sorted in REVERSE date order, so go thru some hoops here...
383
281
revid_list.reverse()
384
index = bisect.bisect(_RevListToTimestamps(revid_list,
385
self._branch.repository),
282
index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
389
285
revid_list.reverse()
405
300
# all the relevant changes (time-consuming) only to return a list of
406
301
# revids which will be used to fetch a set of changes again.
408
# if they entered a revid, just jump straight there;
409
# ignore the passed-in revid_list
303
# if they entered a revid, just jump straight there; ignore the passed-in revid_list
410
304
revid = self.fix_revid(query)
411
305
if revid is not None:
412
306
if isinstance(revid, unicode):
413
307
revid = revid.encode('utf-8')
414
changes = self.get_changes([revid])
308
changes = self.get_changes([ revid ])
415
309
if (changes is not None) and (len(changes) > 0):
419
313
m = self.us_date_re.match(query)
420
314
if m is not None:
421
date = datetime.datetime(util.fix_year(int(m.group(3))),
315
date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
425
317
m = self.earth_date_re.match(query)
426
318
if m is not None:
427
date = datetime.datetime(util.fix_year(int(m.group(3))),
319
date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
431
321
m = self.iso_date_re.match(query)
432
322
if m is not None:
433
date = datetime.datetime(util.fix_year(int(m.group(1))),
323
date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
436
324
if date is not None:
437
325
if revid_list is None:
438
# if no limit to the query was given,
439
# search only the direct-parent path.
326
# if no limit to the query was given, search only the direct-parent path.
440
327
revid_list = list(self.get_revids_from(None, self.last_revid))
441
328
return self.get_revision_history_since(revid_list, date)
591
475
revnol = revno.split(".")
592
476
revnos = ".".join(revnol[:-2])
593
477
revnolast = int(revnol[-1])
594
if revnos in d.keys():
478
if d.has_key(revnos):
596
480
if revnolast < m:
597
d[revnos] = (revnolast, revid)
481
d[revnos] = ( revnolast, revid )
599
d[revnos] = (revnolast, revid)
601
return [d[revnos][1] for revnos in d.keys()]
603
def add_branch_nicks(self, change):
483
d[revnos] = ( revnolast, revid )
485
return [ d[revnos][1] for revnos in d.keys() ]
487
def get_branch_nicks(self, changes):
605
given a 'change', fill in the branch nicks on all parents and merge
489
given a list of changes from L{get_changes}, fill in the branch nicks
490
on all parents and merge points.
608
492
fetch_set = set()
609
for p in change.parents:
610
fetch_set.add(p.revid)
611
for p in change.merge_points:
612
fetch_set.add(p.revid)
493
for change in changes:
494
for p in change.parents:
495
fetch_set.add(p.revid)
496
for p in change.merge_points:
497
fetch_set.add(p.revid)
613
498
p_changes = self.get_changes(list(fetch_set))
614
499
p_change_dict = dict([(c.revid, c) for c in p_changes])
615
for p in change.parents:
616
if p.revid in p_change_dict:
617
p.branch_nick = p_change_dict[p.revid].branch_nick
619
p.branch_nick = '(missing)'
620
for p in change.merge_points:
621
if p.revid in p_change_dict:
622
p.branch_nick = p_change_dict[p.revid].branch_nick
624
p.branch_nick = '(missing)'
500
for change in changes:
501
# arch-converted branches may not have merged branch info :(
502
for p in change.parents:
503
if p.revid in p_change_dict:
504
p.branch_nick = p_change_dict[p.revid].branch_nick
506
p.branch_nick = '(missing)'
507
for p in change.merge_points:
508
if p.revid in p_change_dict:
509
p.branch_nick = p_change_dict[p.revid].branch_nick
511
p.branch_nick = '(missing)'
626
513
def get_changes(self, revid_list):
627
514
"""Return a list of changes objects for the given revids.
667
550
return [self._change_from_revision(rev) for rev in rev_list]
552
def _get_deltas_for_revisions_with_trees(self, revisions):
553
"""Produce a list of revision deltas.
555
Note that the input is a sequence of REVISIONS, not revision_ids.
556
Trees will be held in memory until the generator exits.
557
Each delta is relative to the revision's lefthand predecessor.
558
(This is copied from bzrlib.)
560
required_trees = set()
561
for revision in revisions:
562
required_trees.add(revision.revid)
563
required_trees.update([p.revid for p in revision.parents[:1]])
564
trees = dict((t.get_revision_id(), t) for
565
t in self._branch.repository.revision_trees(required_trees))
567
for revision in revisions:
568
if not revision.parents:
569
old_tree = self._branch.repository.revision_tree(
570
bzrlib.revision.NULL_REVISION)
572
old_tree = trees[revision.parents[0].revid]
573
tree = trees[revision.revid]
574
ret.append(tree.changes_from(old_tree))
669
577
def _change_from_revision(self, revision):
671
579
Given a bzrlib Revision, return a processed "change" for use in
696
598
return util.Container(entry)
698
def get_file_changes_uncached(self, entry):
699
repo = self._branch.repository
701
old_revid = entry.parents[0].revid
703
old_revid = bzrlib.revision.NULL_REVISION
704
return self.file_changes_for_revision_ids(old_revid, entry.revid)
706
def get_file_changes(self, entry):
600
def get_file_changes_uncached(self, entries):
601
delta_list = self._get_deltas_for_revisions_with_trees(entries)
603
return [self.parse_delta(delta) for delta in delta_list]
605
def get_file_changes(self, entries):
707
606
if self._file_change_cache is None:
708
return self.get_file_changes_uncached(entry)
607
return self.get_file_changes_uncached(entries)
710
return self._file_change_cache.get_file_changes(entry)
712
def add_changes(self, entry):
713
changes = self.get_file_changes(entry)
714
entry.changes = changes
609
return self._file_change_cache.get_file_changes(entries)
611
def add_changes(self, entries):
612
changes_list = self.get_file_changes(entries)
614
for entry, changes in zip(entries, changes_list):
615
entry.changes = changes
617
def get_change_with_diff(self, revid, compare_revid=None):
618
change = self.get_changes([revid])[0]
620
if compare_revid is None:
622
compare_revid = change.parents[0].revid
624
compare_revid = 'null:'
626
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
627
rev_tree2 = self._branch.repository.revision_tree(revid)
628
delta = rev_tree2.changes_from(rev_tree1)
630
change.changes = self.parse_delta(delta)
631
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
716
635
def get_file(self, file_id, revid):
717
636
"returns (path, filename, data)"
723
642
path = '/' + path
724
643
return path, inv_entry.name, rev_tree.get_file_text(file_id)
726
def file_changes_for_revision_ids(self, old_revid, new_revid):
645
def _parse_diffs(self, old_tree, new_tree, delta):
647
Return a list of processed diffs, in the format::
656
type: str('context', 'delete', or 'insert'),
665
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
667
process.append((old_path, new_path, fid, kind))
668
for path, fid, kind, text_modified, meta_modified in delta.modified:
669
process.append((path, path, fid, kind))
671
for old_path, new_path, fid, kind in process:
672
old_lines = old_tree.get_file_lines(fid)
673
new_lines = new_tree.get_file_lines(fid)
675
if old_lines != new_lines:
677
bzrlib.diff.internal_diff(old_path, old_lines,
678
new_path, new_lines, buffer)
679
except bzrlib.errors.BinaryFile:
682
diff = buffer.getvalue()
685
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
689
def _process_diff(self, diff):
690
# doesn't really need to be a method; could be static.
693
for line in diff.splitlines():
696
if line.startswith('+++ ') or line.startswith('--- '):
698
if line.startswith('@@ '):
700
if chunk is not None:
702
chunk = util.Container()
704
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
705
old_lineno = lines[0]
706
new_lineno = lines[1]
707
elif line.startswith(' '):
708
chunk.diff.append(util.Container(old_lineno=old_lineno,
709
new_lineno=new_lineno,
714
elif line.startswith('+'):
715
chunk.diff.append(util.Container(old_lineno=None,
716
new_lineno=new_lineno,
717
type='insert', line=line[1:]))
719
elif line.startswith('-'):
720
chunk.diff.append(util.Container(old_lineno=old_lineno,
722
type='delete', line=line[1:]))
725
chunk.diff.append(util.Container(old_lineno=None,
729
if chunk is not None:
733
def parse_delta(self, delta):
728
735
Return a nested data structure containing the changes in a delta::
737
text_changes: list((filename, file_id)),
739
repo = self._branch.repository
740
if bzrlib.revision.is_null(old_revid) or \
741
bzrlib.revision.is_null(new_revid):
742
old_tree, new_tree = map(
743
repo.revision_tree, [old_revid, new_revid])
745
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
747
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
749
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
751
return util.Container(
752
added=sorted(reporter.added, key=lambda x:x.filename),
753
renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
754
removed=sorted(reporter.removed, key=lambda x:x.filename),
755
modified=sorted(reporter.modified, key=lambda x:x.filename),
756
text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))
750
for path, fid, kind in delta.added:
751
added.append((rich_filename(path, kind), fid))
753
for path, fid, kind, text_modified, meta_modified in delta.modified:
754
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
756
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
757
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
758
if meta_modified or text_modified:
759
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
761
for path, fid, kind in delta.removed:
762
removed.append((rich_filename(path, kind), fid))
764
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
767
def add_side_by_side(changes):
768
# FIXME: this is a rotten API.
769
for change in changes:
770
for m in change.changes.modified:
771
m.sbs_chunks = _make_side_by_side(m.chunks)
773
def get_filelist(self, inv, file_id, sort_type=None):
775
return the list of all files (and their attributes) within a given
779
dir_ie = inv[file_id]
780
path = inv.id2path(file_id)
785
for filename, entry in dir_ie.children.iteritems():
786
revid_set.add(entry.revision)
789
for change in self.get_changes(list(revid_set)):
790
change_dict[change.revid] = change
792
for filename, entry in dir_ie.children.iteritems():
794
if entry.kind == 'directory':
797
revid = entry.revision
799
file = util.Container(
800
filename=filename, executable=entry.executable, kind=entry.kind,
801
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
802
revid=revid, change=change_dict[revid])
803
file_list.append(file)
805
if sort_type == 'filename' or sort_type is None:
806
file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
807
elif sort_type == 'size':
808
file_list.sort(key=lambda x: x.size)
809
elif sort_type == 'date':
810
file_list.sort(key=lambda x: x.change.date)
812
# Always sort by kind to get directories first
813
file_list.sort(key=lambda x: x.kind != 'directory')
816
for file in file_list:
823
_BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
825
def annotate_file(self, file_id, revid):
830
file_revid = self.get_inventory(revid)[file_id].revision
832
tree = self._branch.repository.revision_tree(file_revid)
835
for line_revid, text in tree.annotate_iter(file_id):
836
revid_set.add(line_revid)
837
if self._BADCHARS_RE.match(text):
838
# bail out; this isn't displayable text
839
yield util.Container(parity=0, lineno=1, status='same',
840
text='(This is a binary file.)',
841
change=util.Container())
843
change_cache = dict([(c.revid, c) \
844
for c in self.get_changes(list(revid_set))])
846
last_line_revid = None
847
for line_revid, text in tree.annotate_iter(file_id):
848
if line_revid == last_line_revid:
849
# remember which lines have a new revno and which don't
854
last_line_revid = line_revid
855
change = change_cache[line_revid]
856
trunc_revno = change.revno
857
if len(trunc_revno) > 10:
858
trunc_revno = trunc_revno[:9] + '...'
860
yield util.Container(parity=parity, lineno=lineno, status=status,
861
change=change, text=util.fixed_width(text))
864
self.log.debug('annotate: %r secs' % (time.time() - z,))