14
18
# You should have received a copy of the GNU General Public License
15
19
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA
24
# This file (and many of the web templates) contains work based on the
25
# "bazaar-webserve" project by Goffredo Baroncelli, which is in turn based
26
# on "hgweb" by Jake Edge and Matt Mackall.
27
from loggerhead import util
30
class History (object):
36
def from_branch(cls, branch):
37
from breezy import tag
43
import breezy.revision
46
from breezy.transport import NoSuchFile
48
from breezy.errors import NoSuchFile
52
from .wholehistory import compute_whole_history_data
55
def is_branch(folder):
57
breezy.branch.Branch.open(folder)
59
except breezy.errors.NotBranchError:
63
def clean_message(message):
64
"""Clean up a commit message and return it and a short (1-line) version.
66
Commit messages that are long single lines are reflowed using the textwrap
67
module (Robey, the original author of this code, apparently favored this
70
message = message.lstrip().splitlines()
73
message = textwrap.wrap(message[0])
76
# We can end up where when (a) the commit message was empty or (b)
77
# when the message consisted entirely of whitespace, in which case
78
# textwrap.wrap() returns an empty list.
81
# Make short form of commit message.
82
short_message = message[0]
83
if len(short_message) > 60:
84
short_message = short_message[:60] + '...'
86
return message, short_message
89
def rich_filename(path, kind):
90
if kind == 'directory':
97
class _RevListToTimestamps(object):
98
"""This takes a list of revisions, and allows you to bisect by date"""
100
__slots__ = ['revid_list', 'repository']
102
def __init__(self, revid_list, repository):
103
self.revid_list = revid_list
104
self.repository = repository
106
def __getitem__(self, index):
107
"""Get the date of the index'd item"""
108
return datetime.datetime.fromtimestamp(self.repository.get_revision(
109
self.revid_list[index]).timestamp)
112
return len(self.revid_list)
115
class FileChangeReporter(object):
117
def __init__(self, old_tree, new_tree):
122
self.text_changes = []
123
self.old_tree = old_tree
124
self.new_tree = new_tree
126
def revid(self, tree, path):
128
return breezy.revision.NULL_REVISION
130
return tree.get_file_revision(path)
132
return breezy.revision.NULL_REVISION
134
def report(self, paths, versioned, renamed, copied, modified,
137
paths, versioned, renamed, copied,
138
modified, exe_change, kind)
140
def _report(self, paths, versioned, renamed, copied, modified,
142
if modified not in ('unchanged', 'kind changed'):
143
if versioned == 'removed':
144
filename = rich_filename(paths[0], kind[0])
146
filename = rich_filename(paths[1], kind[1])
147
self.text_changes.append(util.Container(
149
old_revision=self.revid(self.old_tree, paths[0]),
150
new_revision=self.revid(self.new_tree, paths[1])))
151
if versioned == 'added':
152
self.added.append(util.Container(
153
filename=rich_filename(paths[1], kind), kind=kind[1]))
154
elif versioned == 'removed':
155
self.removed.append(util.Container(
156
filename=rich_filename(paths[0], kind), kind=kind[0]))
158
self.renamed.append(util.Container(
159
old_filename=rich_filename(paths[0], kind[0]),
160
new_filename=rich_filename(paths[1], kind[1]),
161
text_modified=modified == 'modified', exe_change=exe_change))
163
self.modified.append(util.Container(
164
filename=rich_filename(paths[1], kind),
165
text_modified=modified == 'modified', exe_change=exe_change))
168
# The lru_cache is not thread-safe, so we need a lock around it for
170
rev_info_memory_cache_lock = threading.RLock()
173
class RevInfoMemoryCache(object):
174
"""A store that validates values against the revids they were stored with.
176
We use a unique key for each branch.
178
The reason for not just using the revid as the key is so that when a new
179
value is provided for a branch, we replace the old value used for the
182
There is another implementation of the same interface in
183
loggerhead.changecache.RevInfoDiskCache.
186
def __init__(self, cache):
189
def get(self, key, revid):
190
"""Return the data associated with `key`, subject to a revid check.
192
If a value was stored under `key`, with the same revid, return it.
193
Otherwise return None.
195
rev_info_memory_cache_lock.acquire()
197
cached = self._cache.get(key)
199
rev_info_memory_cache_lock.release()
202
stored_revid, data = cached
203
if revid == stored_revid:
208
def set(self, key, revid, data):
209
"""Store `data` under `key`, to be checked against `revid` on get().
211
rev_info_memory_cache_lock.acquire()
213
self._cache[key] = (revid, data)
215
rev_info_memory_cache_lock.release()
217
# Used to store locks that prevent multiple threads from building a
218
# revision graph for the same branch at the same time, because that can
219
# cause severe performance issues that are so bad that the system seems
221
revision_graph_locks = {}
222
revision_graph_check_lock = threading.Lock()
224
class History(object):
225
"""Decorate a branch to provide information for rendering.
227
History objects are expected to be short lived -- when serving a request
228
for a particular branch, open it, read-lock it, wrap a History object
229
around it, serve the request, throw the History object away, unlock the
230
branch and throw it away.
232
:ivar _rev_info: A list of information about revisions. This is by far
233
the most cryptic data structure in loggerhead. At the top level, it
234
is a list of 3-tuples [(merge-info, where-merged, parents)].
235
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
236
like a merged sorted list, but the revno is stringified.
237
`where-merged` is a tuple of revisions that have this revision as a
238
non-lefthand parent. Finally, `parents` is just the usual list of
239
parents of this revision.
240
:ivar _rev_indices: A dictionary mapping each revision id to the index of
241
the information about it in _rev_info.
242
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
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
# Theoretically, it's possible for two threads to race in creating
262
# the Lock() object for their branch, so we put a lock around
263
# creating the per-branch Lock().
264
revision_graph_check_lock.acquire()
266
if cache_key not in revision_graph_locks:
267
revision_graph_locks[cache_key] = threading.Lock()
269
revision_graph_check_lock.release()
271
revision_graph_locks[cache_key].acquire()
274
data = cache.get(cache_key, self.last_revid)
276
self._rev_info = data
277
update_missed_caches()
280
missed_caches.append(cache)
282
whole_history_data = compute_whole_history_data(self._branch)
283
self._rev_info, self._rev_indices = whole_history_data
284
update_missed_caches()
286
revision_graph_locks[cache_key].release()
288
if self._rev_indices is not None:
289
self._revno_revid = {}
290
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
291
self._revno_revid[revno_str] = revid
293
self._revno_revid = {}
294
self._rev_indices = {}
295
for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
296
self._rev_indices[revid] = seq
297
self._revno_revid[revno_str] = revid
299
def __init__(self, branch, whole_history_data_cache,
300
revinfo_disk_cache=None, cache_key=None):
301
assert branch.is_locked(), (
302
"Can only construct a History object with a read-locked branch.")
38
303
self._branch = branch
39
self._history = branch.revision_history()
40
self._revision_graph = branch.repository.get_revision_graph()
41
self._last_revid = self._history[-1]
43
self._full_history = []
44
self._revision_info = {}
45
self._revno_revid = {}
46
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
48
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
49
self._full_history.append(revid)
50
revno_str = '.'.join(str(n) for n in revno)
51
self._revno_revid[revno_str] = revid
52
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
57
self._where_merged = {}
58
for revid in self._revision_graph.keys():
59
if not revid in self._full_history:
61
for parent in self._revision_graph[revid]:
62
self._where_merged.setdefault(parent, set()).add(revid)
67
def from_folder(cls, path):
68
b = bzrlib.branch.Branch.open(path)
69
return cls.from_branch(b)
71
last_revid = property(lambda self: self._last_revid, None, None)
72
count = property(lambda self: self._count, None, None)
304
self._branch_tags = None
305
self._inventory_cache = {}
306
self._branch_nick = self._branch.get_config().get_nickname()
307
self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
309
self.last_revid = branch.last_revision()
311
caches = [RevInfoMemoryCache(whole_history_data_cache)]
312
if revinfo_disk_cache:
313
caches.append(revinfo_disk_cache)
314
self._load_whole_history_data(caches, cache_key)
317
def has_revisions(self):
318
return not breezy.revision.is_null(self.last_revid)
320
def get_config(self):
321
return self._branch.get_config()
74
323
def get_revno(self, revid):
75
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
78
def get_sequence(self, revid):
79
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
82
def get_short_revision_history(self):
83
return self.get_short_revision_history_from(self._last_revid)
85
def get_short_revision_history_from(self, revid):
86
"""return the short revision_history, starting from revid"""
89
if not self._revision_graph.has_key(revid):
324
if revid not in self._rev_indices:
327
seq = self._rev_indices[revid]
328
revno = self._rev_info[seq][0][3]
331
def get_revids_from(self, revid_list, start_revid):
333
Yield the mainline (wrt start_revid) revisions that merged each
336
if revid_list is None:
337
# Just yield the mainline, starting at start_revid
339
is_null = breezy.revision.is_null
340
while not is_null(revid):
342
parents = self._rev_info[self._rev_indices[revid]][2]
347
revid_set = set(revid_list)
350
def introduced_revisions(revid):
352
seq = self._rev_indices[revid]
353
md = self._rev_info[seq][0][2]
355
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
356
r.add(self._rev_info[i][0][1])
360
if breezy.revision.is_null(revid):
91
parents = self._revision_graph[revid]
362
rev_introduced = introduced_revisions(revid)
363
matching = rev_introduced.intersection(revid_set)
365
# We don't need to look for these anymore.
366
revid_set.difference_update(matching)
368
parents = self._rev_info[self._rev_indices[revid]][2]
92
369
if len(parents) == 0:
94
revid = self._revision_graph[revid][0]
96
def get_where_merged(self, revid):
98
return self._where_merged[revid]
373
def get_short_revision_history_by_fileid(self, file_id):
374
# FIXME: would be awesome if we could get, for a folder, the list of
375
# revisions where items within that folder changed.i
376
# TODO(jelmer): Avoid versionedfile-specific texts
377
possible_keys = [(file_id, revid) for revid in self._rev_indices]
378
get_parent_map = self._branch.repository.texts.get_parent_map
379
# We chunk the requests as this works better with GraphIndex.
380
# See _filter_revisions_touching_file_id in breezy/log.py
381
# for more information.
384
for start in range(0, len(possible_keys), chunk_size):
385
next_keys = possible_keys[start:start + chunk_size]
386
revids += [k[1] for k in get_parent_map(next_keys)]
387
del possible_keys, next_keys
390
def get_revision_history_since(self, revid_list, date):
391
# if a user asks for revisions starting at 01-sep, they mean inclusive,
392
# so start at midnight on 02-sep.
393
date = date + datetime.timedelta(days=1)
394
# our revid list is sorted in REVERSE date order,
395
# so go thru some hoops here...
397
index = bisect.bisect(_RevListToTimestamps(revid_list,
398
self._branch.repository),
404
return revid_list[index:]
406
def get_search_revid_list(self, query, revid_list):
408
given a "quick-search" query, try a few obvious possible meanings:
410
- revision id or # ("128.1.3")
411
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
412
iso style "yyyy-mm-dd")
413
- comment text as a fallback
415
and return a revid list that matches.
417
# FIXME: there is some silliness in this action. we have to look up
418
# all the relevant changes (time-consuming) only to return a list of
419
# revids which will be used to fetch a set of changes again.
421
# if they entered a revid, just jump straight there;
422
# ignore the passed-in revid_list
423
revid = self.fix_revid(query)
424
if revid is not None:
425
changes = self.get_changes([revid])
426
if (changes is not None) and (len(changes) > 0):
430
m = self.us_date_re.match(query)
432
date = datetime.datetime(util.fix_year(int(m.group(3))),
436
m = self.earth_date_re.match(query)
438
date = datetime.datetime(util.fix_year(int(m.group(3))),
442
m = self.iso_date_re.match(query)
444
date = datetime.datetime(util.fix_year(int(m.group(1))),
448
if revid_list is None:
449
# if no limit to the query was given,
450
# search only the direct-parent path.
451
revid_list = list(self.get_revids_from(None, self.last_revid))
452
return self.get_revision_history_since(revid_list, date)
454
revno_re = re.compile(r'^[\d\.]+$')
455
# the date regex are without a final '$' so that queries like
456
# "2006-11-30 12:15" still mostly work. (i think it's better to give
457
# them 90% of what they want instead of nothing at all.)
458
us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
459
earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
460
iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
462
def fix_revid(self, revid):
463
# if a "revid" is actually a dotted revno, convert it to a revid
466
if not isinstance(revid, str):
467
raise TypeError(revid)
469
return self.last_revid
471
if self.revno_re.match(revid):
472
revid = self._revno_revid[revid]
474
raise breezy.errors.NoSuchRevision(self._branch_nick, revid)
475
if not isinstance(revid, bytes):
476
revid = revid.encode('utf-8')
480
def _iterate_sufficiently(iterable, stop_at, extra_rev_count):
481
"""Return a list of iterable.
483
If extra_rev_count is None, fully consume iterable.
484
Otherwise, stop at 'stop_at' + extra_rev_count.
487
iterate until you find stop_at, then iterate 10 more times.
489
if extra_rev_count is None:
490
return list(iterable)
499
for count, n in enumerate(iterable):
500
if count >= extra_rev_count:
505
def _get_file_view(self, revid, file_id):
507
Given a revid and optional path, return a (revlist, revid) for
508
navigation through the current scope: from the revid (or the latest
509
revision) back to the original revision.
511
If file_id is None, the entire revision history is the list scope.
514
revid = self.last_revid
515
if file_id is not None:
517
self.get_short_revision_history_by_fileid(file_id))
518
revlist = self.get_revids_from(revlist, revid)
520
revlist = self.get_revids_from(None, revid)
523
def get_view(self, revid, start_revid, path, query=None,
524
extra_rev_count=None):
526
use the URL parameters (revid, start_revid, path, and query) to
527
determine the revision list we're viewing (start_revid, path, query)
528
and where we are in it (revid).
530
- if a query is given, we're viewing query results.
531
- if a path is given, we're viewing revisions for a specific
533
- if a start_revid is given, we're viewing the branch from a
534
specific revision up the tree.
535
- if extra_rev_count is given, find the view from start_revid =>
536
revid, and continue an additional 'extra_rev_count'. If not
537
given, then revid_list will contain the full history of
540
these may be combined to view revisions for a specific file, from
541
a specific revision, with a specific search query.
543
returns a new (revid, start_revid, revid_list) where:
545
- revid: current position within the view
546
- start_revid: starting revision of this view
547
- revid_list: list of revision ids for this view
549
path and query are never changed so aren't returned, but they may
550
contain vital context for future url navigation.
552
if start_revid is None:
553
start_revid = self.last_revid
556
repo = self._branch.repository
558
file_id = repo.revision_tree(start_revid).path2id(path)
561
revid_list = self._get_file_view(start_revid, file_id)
562
revid_list = self._iterate_sufficiently(revid_list, revid,
566
if revid not in revid_list:
567
# if the given revid is not in the revlist, use a revlist that
568
# starts at the given revid.
569
revid_list = self._get_file_view(revid, file_id)
570
revid_list = self._iterate_sufficiently(revid_list, revid,
573
return revid, start_revid, revid_list
577
# potentially limit the search
578
if file_id is not None:
579
revid_list = self._get_file_view(start_revid, file_id)
582
revid_list = search.search_revisions(self._branch, query)
583
if revid_list and len(revid_list) > 0:
584
if revid not in revid_list:
585
revid = revid_list[0]
586
return revid, start_revid, revid_list
588
# XXX: This should return a message saying that the search could
589
# not be completed due to either missing the plugin or missing a
591
return None, None, []
593
def revision_tree(self, revid):
594
return self._branch.repository.revision_tree(revid)
596
def file_exists(self, revid, path):
597
if (len(path) > 0) and not path.startswith('/'):
600
return self.revision_tree(revid).has_filename(path)
601
except breezy.errors.NoSuchRevision:
102
604
def get_merge_point_list(self, revid):
104
606
Return the list of revids that have merged this node.
106
if revid in self._history:
608
if '.' not in self.get_revno(revid):
111
children = self.get_where_merged(revid)
615
children = self._rev_info[self._rev_indices[revid]][1]
113
616
for child in children:
114
child_parents = self._revision_graph[child]
617
child_parents = self._rev_info[self._rev_indices[child]][2]
115
618
if child_parents[0] == revid:
116
619
nexts.append(child)
118
621
merge_point.append(child)
124
while len(nexts) > 1:
127
merge_point_next = self.get_merge_point_list(next)
128
merge_point.extend(merge_point_next)
132
624
def simplify_merge_point_list(self, revids):
133
625
"""if a revision is already merged, don't show further merge points"""
137
629
revnol = revno.split(".")
138
630
revnos = ".".join(revnol[:-2])
139
631
revnolast = int(revnol[-1])
140
if d.has_key(revnos):
142
634
if revnolast < m:
143
d[revnos] = ( revnolast, revid )
145
d[revnos] = ( revnolast, revid )
147
return [ d[revnos][1] for revnos in d.keys() ]
149
def get_changelist(self, revid_list):
635
d[revnos] = (revnolast, revid)
637
d[revnos] = (revnolast, revid)
639
return [revid for (_, revid) in d.values()]
641
def add_branch_nicks(self, change):
643
given a 'change', fill in the branch nicks on all parents and merge
647
for p in change.parents:
648
fetch_set.add(p.revid)
649
for p in change.merge_points:
650
fetch_set.add(p.revid)
651
p_changes = self.get_changes(list(fetch_set))
652
p_change_dict = {c.revid: c for c in p_changes}
653
for p in change.parents:
654
if p.revid in p_change_dict:
655
p.branch_nick = p_change_dict[p.revid].branch_nick
657
p.branch_nick = '(missing)'
658
for p in change.merge_points:
659
if p.revid in p_change_dict:
660
p.branch_nick = p_change_dict[p.revid].branch_nick
662
p.branch_nick = '(missing)'
664
def get_changes(self, revid_list):
665
"""Return a list of changes objects for the given revids.
667
Revisions not present and NULL_REVISION will be ignored.
150
669
for revid in revid_list:
151
yield self.get_change(revid)
153
def get_change(self, revid, parity=0):
155
rev = self._branch.repository.get_revision(revid)
156
except (KeyError, bzrlib.errors.NoSuchRevision):
161
'date': datetime.datetime.fromtimestamp(0),
164
'short_comment': 'missing',
167
return util.Container(entry)
169
now = datetime.datetime.now()
170
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
172
# make short form of commit message
173
short_comment = rev.message.splitlines(1)[0]
174
if len(short_comment) >= 80:
175
short_comment = textwrap.wrap(short_comment)[0] + '...'
178
'revid': parent_revid,
179
'revno': self.get_revno(parent_revid),
180
} for parent_revid in rev.parent_ids]
670
if not isinstance(revid, bytes):
671
raise TypeError(revid_list)
672
changes = self.get_changes_uncached(revid_list)
673
if len(changes) == 0:
676
# some data needs to be recalculated each time, because it may
677
# change as new revisions are added.
678
for change in changes:
679
merge_revids = self.simplify_merge_point_list(
680
self.get_merge_point_list(change.revid))
681
change.merge_points = [
682
util.Container(revid=r,
683
revno=self.get_revno(r)) for r in merge_revids]
684
if len(change.parents) > 0:
685
change.parents = [util.Container(revid=r,
686
revno=self.get_revno(r)) for r in change.parents]
687
change.revno = self.get_revno(change.revid)
690
for change in changes:
691
change.parity = parity
696
def get_changes_uncached(self, revid_list):
697
# FIXME: deprecated method in getting a null revision
698
revid_list = list(filter(lambda revid: not breezy.revision.is_null(revid),
700
parent_map = self._branch.repository.get_graph().get_parent_map(
702
# We need to return the answer in the same order as the input,
704
present_revids = [revid for revid in revid_list
705
if revid in parent_map]
706
rev_list = self._branch.repository.get_revisions(present_revids)
708
return [self._change_from_revision(rev) for rev in rev_list]
710
def _change_from_revision(self, revision):
712
Given a breezy Revision, return a processed "change" for use in
715
message, short_message = clean_message(revision.message)
717
if self._branch_tags is None:
718
self._branch_tags = self._branch.tags.get_reverse_tag_dict()
721
if revision.revision_id in self._branch_tags:
722
# tag.sort_* functions expect (tag, data) pairs, so we generate them,
723
# and then strip them
724
tags = [(t, None) for t in self._branch_tags[revision.revision_id]]
725
sort_func = getattr(tag, 'sort_natural', None)
726
if sort_func is None:
729
sort_func(self._branch, tags)
730
revtags = u', '.join([t[0] for t in tags])
184
'revno': self.get_revno(revid),
186
'author': rev.committer,
187
'age': util.timespan(now - commit_time) + ' ago',
188
'short_comment': short_comment,
189
'comment': rev.message,
190
'parents': [util.Container(p) for p in parents],
733
'revid': revision.revision_id,
734
'date': datetime.datetime.fromtimestamp(revision.timestamp),
735
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
736
'timestamp': revision.timestamp,
737
'committer': revision.committer,
738
'authors': revision.get_apparent_authors(),
739
'branch_nick': revision.properties.get('branch-nick', None),
740
'short_comment': short_message,
741
'comment': revision.message,
742
'comment_clean': [util.html_clean(s) for s in message],
743
'parents': revision.parent_ids,
744
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
747
if isinstance(revision, breezy.foreign.ForeignRevision):
748
foreign_revid, mapping = (
749
revision.foreign_revid, revision.mapping)
750
elif b":" in revision.revision_id:
752
foreign_revid, mapping = \
753
breezy.foreign.foreign_vcs_registry.parse_revision_id(
754
revision.revision_id)
755
except breezy.errors.InvalidRevisionId:
760
if foreign_revid is not None:
761
entry["foreign_vcs"] = mapping.vcs.abbreviation
762
entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
192
763
return util.Container(entry)
194
def scan_range(self, revid, pagesize=20):
196
yield a list of (label, revid) for a scan range through the full
197
branch history, centered around the given revid.
199
example: [ ('(1)', 'first-revid'), ('-300', '...'), ... ]
202
pos = self.get_sequence(revid)
204
yield ('<', self._full_history[min(count - 1, pos + pagesize)])
207
yield ('(1)', self._full_history[-1])
208
for offset in reversed([-x for x in util.scan_range(pos, count)]):
209
yield ('%+d' % (offset,), self._full_history[pos - offset])
211
yield ('>', self._full_history[max(0, pos - pagesize)])
765
def get_file_changes(self, entry):
767
old_revid = entry.parents[0].revid
769
old_revid = breezy.revision.NULL_REVISION
770
return self.file_changes_for_revision_ids(old_revid, entry.revid)
772
def add_changes(self, entry):
773
changes = self.get_file_changes(entry)
774
entry.changes = changes
776
def get_file(self, path, revid):
777
"""Returns (path, filename, file contents)"""
778
if not isinstance(path, str):
779
raise TypeError(path)
780
if not isinstance(revid, bytes):
781
raise TypeError(revid)
782
rev_tree = self._branch.repository.revision_tree(revid)
784
if not display_path.startswith('/'):
786
return (display_path, breezy.osutils.basename(path),
787
rev_tree.get_file_text(path))
789
def file_changes_for_revision_ids(self, old_revid, new_revid):
791
Return a nested data structure containing the changes in a delta::
793
added: list((filename)),
794
renamed: list((old_filename, new_filename)),
795
deleted: list((filename)),
796
modified: list((filename)),
797
text_changes: list((filename)),
799
repo = self._branch.repository
800
if (breezy.revision.is_null(old_revid) or
801
breezy.revision.is_null(new_revid) or
802
old_revid == new_revid):
803
old_tree, new_tree = map(
804
repo.revision_tree, [old_revid, new_revid])
806
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
808
reporter = FileChangeReporter(old_tree, new_tree)
810
breezy.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
812
return util.Container(
813
added=sorted(reporter.added, key=lambda x: x.filename),
814
renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
815
removed=sorted(reporter.removed, key=lambda x: x.filename),
816
modified=sorted(reporter.modified, key=lambda x: x.filename),
817
text_changes=sorted(reporter.text_changes,
818
key=lambda x: x.filename))