16
21
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
# This file (and many of the web templates) contains work based on the
26
# "bazaar-webserve" project by Goffredo Baroncelli, which is in turn based
27
# on "hgweb" by Jake Edge and Matt Mackall.
28
from StringIO import StringIO
31
import bzrlib.annotate
32
38
import bzrlib.branch
34
40
import bzrlib.errors
35
import bzrlib.textfile
42
import bzrlib.revision
44
from loggerhead import search
38
45
from loggerhead import util
40
log = logging.getLogger("loggerhead.controllers")
43
class History (object):
46
self._change_cache = None
47
log.error('new history: %r' % (self,))
51
if self._change_cache is not None:
52
self._change_cache.close()
53
self._change_cache_diffs.close()
54
self._change_cache = None
55
self._change_cache_diffs = None
58
def from_branch(cls, branch):
46
from loggerhead.wholehistory import compute_whole_history_data
49
def is_branch(folder):
51
bzrlib.branch.Branch.open(folder)
57
def clean_message(message):
58
"""Clean up a commit message and return it and a short (1-line) version.
60
Commit messages that are long single lines are reflowed using the textwrap
61
module (Robey, the original author of this code, apparently favored this
64
message = message.lstrip().splitlines()
67
message = textwrap.wrap(message[0])
70
# We can end up where when (a) the commit message was empty or (b)
71
# when the message consisted entirely of whitespace, in which case
72
# textwrap.wrap() returns an empty list.
75
# Make short form of commit message.
76
short_message = message[0]
77
if len(short_message) > 60:
78
short_message = short_message[:60] + '...'
80
return message, short_message
83
def rich_filename(path, kind):
84
if kind == 'directory':
91
class _RevListToTimestamps(object):
92
"""This takes a list of revisions, and allows you to bisect by date"""
94
__slots__ = ['revid_list', 'repository']
96
def __init__(self, revid_list, repository):
97
self.revid_list = revid_list
98
self.repository = repository
100
def __getitem__(self, index):
101
"""Get the date of the index'd item"""
102
return datetime.datetime.fromtimestamp(self.repository.get_revision(
103
self.revid_list[index]).timestamp)
106
return len(self.revid_list)
108
class FileChangeReporter(object):
110
def __init__(self, old_inv, new_inv):
115
self.text_changes = []
116
self.old_inv = old_inv
117
self.new_inv = new_inv
119
def revid(self, inv, file_id):
121
return inv[file_id].revision
122
except bzrlib.errors.NoSuchId:
125
def report(self, file_id, paths, versioned, renamed, modified,
127
if modified not in ('unchanged', 'kind changed'):
128
if versioned == 'removed':
129
filename = rich_filename(paths[0], kind[0])
131
filename = rich_filename(paths[1], kind[1])
132
self.text_changes.append(util.Container(
133
filename=filename, file_id=file_id,
134
old_revision=self.revid(self.old_inv, file_id),
135
new_revision=self.revid(self.new_inv, file_id)))
136
if versioned == 'added':
137
self.added.append(util.Container(
138
filename=rich_filename(paths[1], kind),
139
file_id=file_id, kind=kind[1]))
140
elif versioned == 'removed':
141
self.removed.append(util.Container(
142
filename=rich_filename(paths[0], kind),
143
file_id=file_id, kind=kind[0]))
145
self.renamed.append(util.Container(
146
old_filename=rich_filename(paths[0], kind[0]),
147
new_filename=rich_filename(paths[1], kind[1]),
149
text_modified=modified == 'modified'))
151
self.modified.append(util.Container(
152
filename=rich_filename(paths[1], kind),
155
# The lru_cache is not thread-safe, so we need a lock around it for
157
rev_info_memory_cache_lock = threading.RLock()
159
class RevInfoMemoryCache(object):
160
"""A store that validates values against the revids they were stored with.
162
We use a unique key for each branch.
164
The reason for not just using the revid as the key is so that when a new
165
value is provided for a branch, we replace the old value used for the
168
There is another implementation of the same interface in
169
loggerhead.changecache.RevInfoDiskCache.
172
def __init__(self, cache):
175
def get(self, key, revid):
176
"""Return the data associated with `key`, subject to a revid check.
178
If a value was stored under `key`, with the same revid, return it.
179
Otherwise return None.
181
rev_info_memory_cache_lock.acquire()
183
cached = self._cache.get(key)
185
rev_info_memory_cache_lock.release()
188
stored_revid, data = cached
189
if revid == stored_revid:
194
def set(self, key, revid, data):
195
"""Store `data` under `key`, to be checked against `revid` on get().
197
rev_info_memory_cache_lock.acquire()
199
self._cache[key] = (revid, data)
201
rev_info_memory_cache_lock.release()
203
# Used to store locks that prevent multiple threads from building a
204
# revision graph for the same branch at the same time, because that can
205
# cause severe performance issues that are so bad that the system seems
207
revision_graph_locks = {}
208
revision_graph_check_lock = threading.Lock()
210
class History(object):
211
"""Decorate a branch to provide information for rendering.
213
History objects are expected to be short lived -- when serving a request
214
for a particular branch, open it, read-lock it, wrap a History object
215
around it, serve the request, throw the History object away, unlock the
216
branch and throw it away.
218
:ivar _file_change_cache: An object that caches information about the
219
files that changed between two revisions.
220
:ivar _rev_info: A list of information about revisions. This is by far
221
the most cryptic data structure in loggerhead. At the top level, it
222
is a list of 3-tuples [(merge-info, where-merged, parents)].
223
`merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
224
like a merged sorted list, but the revno is stringified.
225
`where-merged` is a tuple of revisions that have this revision as a
226
non-lefthand parent. Finally, `parents` is just the usual list of
227
parents of this revision.
228
:ivar _rev_indices: A dictionary mapping each revision id to the index of
229
the information about it in _rev_info.
230
:ivar _revno_revid: A dictionary mapping stringified revnos to revision
234
def _load_whole_history_data(self, caches, cache_key):
235
"""Set the attributes relating to the whole history of the branch.
237
:param caches: a list of caches with interfaces like
238
`RevInfoMemoryCache` and be ordered from fastest to slowest.
239
:param cache_key: the key to use with the caches.
241
self._rev_indices = None
242
self._rev_info = None
245
def update_missed_caches():
246
for cache in missed_caches:
247
cache.set(cache_key, self.last_revid, self._rev_info)
249
# Theoretically, it's possible for two threads to race in creating
250
# the Lock() object for their branch, so we put a lock around
251
# creating the per-branch Lock().
252
revision_graph_check_lock.acquire()
254
if cache_key not in revision_graph_locks:
255
revision_graph_locks[cache_key] = threading.Lock()
257
revision_graph_check_lock.release()
259
revision_graph_locks[cache_key].acquire()
262
data = cache.get(cache_key, self.last_revid)
264
self._rev_info = data
265
update_missed_caches()
268
missed_caches.append(cache)
270
whole_history_data = compute_whole_history_data(self._branch)
271
self._rev_info, self._rev_indices = whole_history_data
272
update_missed_caches()
274
revision_graph_locks[cache_key].release()
276
if self._rev_indices is not None:
277
self._revno_revid = {}
278
for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
279
self._revno_revid[revno_str] = revid
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
287
def __init__(self, branch, whole_history_data_cache, file_cache=None,
288
revinfo_disk_cache=None, cache_key=None):
289
assert branch.is_locked(), (
290
"Can only construct a History object with a read-locked branch.")
291
if file_cache is not None:
292
self._file_change_cache = file_cache
293
file_cache.history = self
295
self._file_change_cache = None
61
296
self._branch = branch
62
self._history = branch.revision_history()
63
self._revision_graph = branch.repository.get_revision_graph()
64
self._last_revid = self._history[-1]
66
self._full_history = []
67
self._revision_info = {}
68
self._revno_revid = {}
69
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
71
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
72
self._full_history.append(revid)
73
revno_str = '.'.join(str(n) for n in revno)
74
self._revno_revid[revno_str] = revid
75
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
80
self._where_merged = {}
81
for revid in self._revision_graph.keys():
82
if not revid in self._full_history:
84
for parent in self._revision_graph[revid]:
85
self._where_merged.setdefault(parent, set()).add(revid)
87
log.info('built revision graph cache: %r secs' % (time.time() - z,))
91
def from_folder(cls, path):
92
b = bzrlib.branch.Branch.open(path)
93
return cls.from_branch(b)
95
def out_of_date(self):
96
if self._branch.revision_history()[-1] != self._last_revid:
100
def use_cache(self, path):
101
if not os.path.exists(path):
103
# keep a separate cache for the diffs, because they're very time-consuming to fetch.
104
cachefile = os.path.join(path, 'changes')
105
cachefile_diffs = os.path.join(path, 'changes-diffs')
107
# why can't shelve allow 'cw'?
108
if not os.path.exists(cachefile):
109
self._change_cache = shelve.open(cachefile, 'c', protocol=2)
111
self._change_cache = shelve.open(cachefile, 'w', protocol=2)
112
if not os.path.exists(cachefile_diffs):
113
self._change_cache_diffs = shelve.open(cachefile_diffs, 'c', protocol=2)
115
self._change_cache_diffs = shelve.open(cachefile_diffs, 'w', protocol=2)
117
# once we process a change (revision), it should be the same forever.
118
log.info('Using change cache %s; %d, %d entries.' % (path, len(self._change_cache), len(self._change_cache_diffs)))
119
self._cache_lock = threading.Lock()
120
self._change_cache_filename = cachefile
121
self._change_cache_diffs_filename = cachefile_diffs
123
def dont_use_cache(self):
124
# called when a new history object needs to be created. we can't use
125
# the cache files anymore; they belong to the new history object.
126
self._cache_lock.acquire()
128
if self._change_cache is None:
130
self._change_cache.close()
131
self._change_cache_diffs.close()
132
self._change_cache = None
133
self._change_cache_diffs = None
135
self._cache_lock.release()
137
def flush_cache(self):
138
# shelve seems to need the file to be closed to save anything :(
139
self._cache_lock.acquire()
141
log.info('flush cache: %r (from %r)' % (len(self._change_cache), threading.currentThread()))
142
if self._change_cache is None:
144
self._change_cache.sync()
145
self._change_cache_diffs.sync()
147
self._cache_lock.release()
149
last_revid = property(lambda self: self._last_revid, None, None)
151
count = property(lambda self: self._count, None, None)
153
def get_revision(self, revid):
154
return self._branch.repository.get_revision(revid)
297
self._branch_tags = None
298
self._inventory_cache = {}
299
self._branch_nick = self._branch.get_config().get_nickname()
300
self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
302
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)
310
def has_revisions(self):
311
return not bzrlib.revision.is_null(self.last_revid)
313
def get_config(self):
314
return self._branch.get_config()
156
316
def get_revno(self, revid):
157
if revid not in self._revision_info:
317
if revid not in self._rev_indices:
160
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
163
def get_sequence(self, revid):
164
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
167
def get_revision_history(self):
168
return self._full_history
170
def get_revid_sequence(self, revid_list, revid):
172
given a list of revision ids, return the sequence # of this revid in
181
def get_revids_from(self, revid_list, revid):
183
given a list of revision ids, yield revisions in graph order,
184
starting from revid. the list can be None if you just want to travel
185
across all revisions.
320
seq = self._rev_indices[revid]
321
revno = self._rev_info[seq][0][3]
324
def get_revids_from(self, revid_list, start_revid):
326
Yield the mainline (wrt start_revid) revisions that merged each
329
if revid_list is None:
330
revid_list = [r[0][1] for r in self._rev_info]
331
revid_set = set(revid_list)
334
def introduced_revisions(revid):
336
seq = self._rev_indices[revid]
337
md = self._rev_info[seq][0][2]
339
while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
340
r.add(self._rev_info[i][0][1])
188
if (revid_list is None) or (revid in revid_list):
344
if bzrlib.revision.is_null(revid):
346
if introduced_revisions(revid) & revid_set:
190
if not self._revision_graph.has_key(revid):
192
parents = self._revision_graph[revid]
348
parents = self._rev_info[self._rev_indices[revid]][2]
193
349
if len(parents) == 0:
195
351
revid = parents[0]
197
353
def get_short_revision_history_by_fileid(self, file_id):
198
# wow. is this really the only way we can get this list? by
199
# man-handling the weave store directly? :-0
200
354
# FIXME: would be awesome if we could get, for a folder, the list of
201
# revisions where items within that folder changed.
202
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
203
w_revids = w.versions()
204
revids = [r for r in self._full_history if r in w_revids]
355
# revisions where items within that folder changed.i
356
possible_keys = [(file_id, revid) for revid in self._rev_indices]
357
get_parent_map = self._branch.repository.texts.get_parent_map
358
# We chunk the requests as this works better with GraphIndex.
359
# See _filter_revisions_touching_file_id in bzrlib/log.py
360
# for more information.
363
for start in xrange(0, len(possible_keys), chunk_size):
364
next_keys = possible_keys[start:start + chunk_size]
365
revids += [k[1] for k in get_parent_map(next_keys)]
366
del possible_keys, next_keys
207
def get_navigation(self, revid, path):
209
Given an optional revid and optional path, return a (revlist, revid)
210
for navigation through the current scope: from the revid (or the
211
latest revision) back to the original revision.
213
If path is None, the entire revision history is the list scope.
214
If revid is None, the latest revision is used.
217
revid = self._last_revid
219
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
220
inv = self._branch.repository.get_revision_inventory(revid)
221
revlist = list(self.get_short_revision_history_by_fileid(inv.path2id(path)))
369
def get_revision_history_since(self, revid_list, date):
370
# if a user asks for revisions starting at 01-sep, they mean inclusive,
371
# so start at midnight on 02-sep.
372
date = date + datetime.timedelta(days=1)
373
# our revid list is sorted in REVERSE date order,
374
# so go thru some hoops here...
376
index = bisect.bisect(_RevListToTimestamps(revid_list,
377
self._branch.repository),
383
return revid_list[index:]
385
def get_search_revid_list(self, query, revid_list):
387
given a "quick-search" query, try a few obvious possible meanings:
389
- revision id or # ("128.1.3")
390
- date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
391
iso style "yyyy-mm-dd")
392
- comment text as a fallback
394
and return a revid list that matches.
396
# FIXME: there is some silliness in this action. we have to look up
397
# all the relevant changes (time-consuming) only to return a list of
398
# revids which will be used to fetch a set of changes again.
400
# if they entered a revid, just jump straight there;
401
# ignore the passed-in revid_list
402
revid = self.fix_revid(query)
403
if revid is not None:
404
if isinstance(revid, unicode):
405
revid = revid.encode('utf-8')
406
changes = self.get_changes([revid])
407
if (changes is not None) and (len(changes) > 0):
411
m = self.us_date_re.match(query)
413
date = datetime.datetime(util.fix_year(int(m.group(3))),
417
m = self.earth_date_re.match(query)
419
date = datetime.datetime(util.fix_year(int(m.group(3))),
423
m = self.iso_date_re.match(query)
425
date = datetime.datetime(util.fix_year(int(m.group(1))),
429
if revid_list is None:
430
# if no limit to the query was given,
431
# search only the direct-parent path.
432
revid_list = list(self.get_revids_from(None, self.last_revid))
433
return self.get_revision_history_since(revid_list, date)
435
revno_re = re.compile(r'^[\d\.]+$')
436
# the date regex are without a final '$' so that queries like
437
# "2006-11-30 12:15" still mostly work. (i think it's better to give
438
# them 90% of what they want instead of nothing at all.)
439
us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
440
earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
441
iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
443
def fix_revid(self, revid):
444
# if a "revid" is actually a dotted revno, convert it to a revid
448
return self.last_revid
450
if self.revno_re.match(revid):
451
revid = self._revno_revid[revid]
453
raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
456
def get_file_view(self, revid, file_id):
458
Given a revid and optional path, return a (revlist, revid) for
459
navigation through the current scope: from the revid (or the latest
460
revision) back to the original revision.
462
If file_id is None, the entire revision history is the list scope.
465
revid = self.last_revid
466
if file_id is not None:
467
# since revid is 'start_revid', possibly should start the path
468
# tracing from revid... FIXME
469
revlist = list(self.get_short_revision_history_by_fileid(file_id))
470
revlist = list(self.get_revids_from(revlist, revid))
223
472
revlist = list(self.get_revids_from(None, revid))
226
return revlist, revid
475
def get_view(self, revid, start_revid, file_id, query=None):
477
use the URL parameters (revid, start_revid, file_id, and query) to
478
determine the revision list we're viewing (start_revid, file_id, query)
479
and where we are in it (revid).
481
- if a query is given, we're viewing query results.
482
- if a file_id is given, we're viewing revisions for a specific
484
- if a start_revid is given, we're viewing the branch from a
485
specific revision up the tree.
487
these may be combined to view revisions for a specific file, from
488
a specific revision, with a specific search query.
490
returns a new (revid, start_revid, revid_list) where:
492
- revid: current position within the view
493
- start_revid: starting revision of this view
494
- revid_list: list of revision ids for this view
496
file_id and query are never changed so aren't returned, but they may
497
contain vital context for future url navigation.
499
if start_revid is None:
500
start_revid = self.last_revid
503
revid_list = self.get_file_view(start_revid, file_id)
506
if revid not in revid_list:
507
# if the given revid is not in the revlist, use a revlist that
508
# starts at the given revid.
509
revid_list = self.get_file_view(revid, file_id)
511
return revid, start_revid, revid_list
513
# potentially limit the search
514
if file_id is not None:
515
revid_list = self.get_file_view(start_revid, file_id)
518
revid_list = search.search_revisions(self._branch, query)
519
if revid_list and len(revid_list) > 0:
520
if revid not in revid_list:
521
revid = revid_list[0]
522
return revid, start_revid, revid_list
524
# XXX: This should return a message saying that the search could
525
# not be completed due to either missing the plugin or missing a
527
return None, None, []
228
529
def get_inventory(self, revid):
229
return self._branch.repository.get_revision_inventory(revid)
231
def get_where_merged(self, revid):
233
return self._where_merged[revid]
530
if revid not in self._inventory_cache:
531
self._inventory_cache[revid] = (
532
self._branch.repository.get_inventory(revid))
533
return self._inventory_cache[revid]
535
def get_path(self, revid, file_id):
536
if (file_id is None) or (file_id == ''):
538
path = self.get_inventory(revid).id2path(file_id)
539
if (len(path) > 0) and not path.startswith('/'):
543
def get_file_id(self, revid, path):
544
if (len(path) > 0) and not path.startswith('/'):
546
return self.get_inventory(revid).path2id(path)
237
548
def get_merge_point_list(self, revid):
239
550
Return the list of revids that have merged this node.
241
if revid in self._history:
552
if '.' not in self.get_revno(revid):
246
children = self.get_where_merged(revid)
557
children = self._rev_info[self._rev_indices[revid]][1]
248
559
for child in children:
249
child_parents = self._revision_graph[child]
560
child_parents = self._rev_info[self._rev_indices[child]][2]
250
561
if child_parents[0] == revid:
251
562
nexts.append(child)
272
583
revnol = revno.split(".")
273
584
revnos = ".".join(revnol[:-2])
274
585
revnolast = int(revnol[-1])
275
if d.has_key(revnos):
277
588
if revnolast < m:
278
d[revnos] = ( revnolast, revid )
280
d[revnos] = ( revnolast, revid )
282
return [ d[revnos][1] for revnos in d.keys() ]
284
def get_changelist(self, revid_list):
285
for revid in revid_list:
286
yield self.get_change(revid)
288
def get_change(self, revid, get_diffs=False):
289
if self._change_cache is None:
290
return self._get_change(revid, get_diffs)
292
# if the revid is in unicode, use the utf-8 encoding as the key
294
if isinstance(revid, unicode):
295
srevid = revid.encode('utf-8')
296
self._cache_lock.acquire()
299
cache = self._change_cache_diffs
301
cache = self._change_cache
306
if get_diffs and (srevid in self._change_cache):
307
# salvage the non-diff entry for a jump-start
308
c = self._change_cache[srevid]
309
if len(change.parents) == 0:
312
left_parent = change.parents[0].revid
313
c.changes = self.diff_revisions(revid, left_parent, get_diffs=True)
316
#log.debug('Entry cache miss: %r' % (revid,))
317
c = self._get_change(revid, get_diffs=get_diffs)
320
# some data needs to be recalculated each time, because it may
321
# change as new revisions are added.
322
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
323
c.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
327
self._cache_lock.release()
329
def _get_change(self, revid, get_diffs=False):
331
rev = self._branch.repository.get_revision(revid)
332
except (KeyError, bzrlib.errors.NoSuchRevision):
337
'date': datetime.datetime.fromtimestamp(0),
340
'short_comment': 'missing',
341
'comment': 'missing',
342
'comment_clean': 'missing',
347
log.error('ghost entry: %r' % (revid,))
348
return util.Container(entry)
350
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
352
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
354
if len(parents) == 0:
357
left_parent = rev.parent_ids[0]
359
message = rev.message.splitlines()
360
if len(message) == 1:
361
# robey-style 1-line long message
362
message = textwrap.wrap(message[0])
364
# make short form of commit message
365
short_message = message[0]
366
if len(short_message) > 60:
367
short_message = short_message[:60] + '...'
589
d[revnos] = (revnolast, revid)
591
d[revnos] = (revnolast, revid)
593
return [revid for (_, revid) in d.itervalues()]
595
def add_branch_nicks(self, change):
597
given a 'change', fill in the branch nicks on all parents and merge
601
for p in change.parents:
602
fetch_set.add(p.revid)
603
for p in change.merge_points:
604
fetch_set.add(p.revid)
605
p_changes = self.get_changes(list(fetch_set))
606
p_change_dict = dict([(c.revid, c) for c in p_changes])
607
for p in change.parents:
608
if p.revid in p_change_dict:
609
p.branch_nick = p_change_dict[p.revid].branch_nick
611
p.branch_nick = '(missing)'
612
for p in change.merge_points:
613
if p.revid in p_change_dict:
614
p.branch_nick = p_change_dict[p.revid].branch_nick
616
p.branch_nick = '(missing)'
618
def get_changes(self, revid_list):
619
"""Return a list of changes objects for the given revids.
621
Revisions not present and NULL_REVISION will be ignored.
623
changes = self.get_changes_uncached(revid_list)
624
if len(changes) == 0:
627
# some data needs to be recalculated each time, because it may
628
# change as new revisions are added.
629
for change in changes:
630
merge_revids = self.simplify_merge_point_list(
631
self.get_merge_point_list(change.revid))
632
change.merge_points = [
633
util.Container(revid=r,
634
revno=self.get_revno(r)) for r in merge_revids]
635
if len(change.parents) > 0:
636
change.parents = [util.Container(revid=r,
637
revno=self.get_revno(r)) for r in change.parents]
638
change.revno = self.get_revno(change.revid)
641
for change in changes:
642
change.parity = parity
647
def get_changes_uncached(self, revid_list):
648
# FIXME: deprecated method in getting a null revision
649
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
651
parent_map = self._branch.repository.get_graph().get_parent_map(
653
# We need to return the answer in the same order as the input,
655
present_revids = [revid for revid in revid_list
656
if revid in parent_map]
657
rev_list = self._branch.repository.get_revisions(present_revids)
659
return [self._change_from_revision(rev) for rev in rev_list]
661
def _change_from_revision(self, revision):
663
Given a bzrlib Revision, return a processed "change" for use in
666
message, short_message = clean_message(revision.message)
668
if self._branch_tags is None:
669
self._branch_tags = self._branch.tags.get_reverse_tag_dict()
672
if revision.revision_id in self._branch_tags:
673
revtags = ', '.join(self._branch_tags[revision.revision_id])
371
'revno': self.get_revno(revid),
373
'author': rev.committer,
374
'branch_nick': rev.properties.get('branch-nick', None),
676
'revid': revision.revision_id,
677
'date': datetime.datetime.fromtimestamp(revision.timestamp),
678
'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
679
'authors': revision.get_apparent_authors(),
680
'branch_nick': revision.properties.get('branch-nick', None),
375
681
'short_comment': short_message,
376
'comment': rev.message,
682
'comment': revision.message,
377
683
'comment_clean': [util.html_clean(s) for s in message],
379
'changes': self.diff_revisions(revid, left_parent, get_diffs=get_diffs),
684
'parents': revision.parent_ids,
685
'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
688
if isinstance(revision, bzrlib.foreign.ForeignRevision):
689
foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
690
elif ":" in revision.revision_id:
692
foreign_revid, mapping = \
693
bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
694
revision.revision_id)
695
except bzrlib.errors.InvalidRevisionId:
700
if foreign_revid is not None:
701
entry["foreign_vcs"] = mapping.vcs.abbreviation
702
entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
381
703
return util.Container(entry)
383
def scan_range(self, revlist, revid, pagesize=20):
385
yield a list of (label, title, revid) for a scan range through the full
386
branch history, centered around the given revid.
388
example: [ ('(425)', 'Latest', 'rrrr'), ('+1', 'Forward 1', 'rrrr'), ...
389
('-300', 'Back 300', 'rrrr'), ('(1)', 'Oldest', 'first-revid') ]
392
pos = self.get_revid_sequence(revlist, revid)
394
yield ('<', 'Back %d' % (pagesize,),
395
revlist[min(count - 1, pos + pagesize)])
397
yield ('<', None, None)
398
yield ('(1)', 'Oldest', revlist[-1])
399
for offset in reversed([-x for x in util.scan_range(pos, count)]):
401
title = 'Back %d' % (-offset,)
403
title = 'Forward %d' % (offset,)
404
yield ('%+d' % (offset,), title, revlist[pos - offset])
405
yield ('(%d)' % (len(revlist),) , 'Latest', revlist[0])
407
yield ('>', 'Forward %d' % (pagesize,),
408
revlist[max(0, pos - pagesize)])
410
yield ('>', None, None)
412
def get_revlist_offset(self, revlist, revid, offset):
414
pos = self.get_revid_sequence(revlist, revid)
416
return revlist[max(0, pos + offset)]
417
return revlist[min(count - 1, pos + offset)]
419
def diff_revisions(self, revid, otherrevid, get_diffs=True):
421
Return a nested data structure containing the changes between two
424
added: list(filename),
425
renamed: list((old_filename, new_filename)),
426
deleted: list(filename),
705
def get_file_changes_uncached(self, entry):
707
old_revid = entry.parents[0].revid
709
old_revid = bzrlib.revision.NULL_REVISION
710
return self.file_changes_for_revision_ids(old_revid, entry.revid)
712
def get_file_changes(self, entry):
713
if self._file_change_cache is None:
714
return self.get_file_changes_uncached(entry)
716
return self._file_change_cache.get_file_changes(entry)
718
def add_changes(self, entry):
719
changes = self.get_file_changes(entry)
720
entry.changes = changes
722
def get_file(self, file_id, revid):
723
"""Returns (path, filename, file contents)"""
724
inv = self.get_inventory(revid)
725
inv_entry = inv[file_id]
726
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
727
path = inv.id2path(file_id)
728
if not path.startswith('/'):
730
return path, inv_entry.name, rev_tree.get_file_text(file_id)
732
def file_changes_for_revision_ids(self, old_revid, new_revid):
734
Return a nested data structure containing the changes in a delta::
736
added: list((filename, file_id)),
737
renamed: list((old_filename, new_filename, file_id)),
738
deleted: list((filename, file_id)),
433
type: str('context', 'delete', or 'insert'),
439
if C{get_diffs} is false, the C{chunks} will be omitted.
442
new_tree = self._branch.repository.revision_tree(revid)
443
old_tree = self._branch.repository.revision_tree(otherrevid)
444
delta = new_tree.changes_from(old_tree)
451
def rich_filename(path, kind):
452
if kind == 'directory':
454
if kind == 'symlink':
458
def tree_lines(tree, fid):
461
tree_file = bzrlib.textfile.text_file(tree.get_file(fid))
462
return tree_file.readlines()
464
def process_diff(diff):
467
for line in diff.splitlines():
470
if line.startswith('+++ ') or line.startswith('--- '):
472
if line.startswith('@@ '):
474
if chunk is not None:
476
chunk = util.Container()
478
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
479
old_lineno = lines[0]
480
new_lineno = lines[1]
481
elif line.startswith(' '):
482
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
483
type='context', line=util.html_clean(line[1:])))
486
elif line.startswith('+'):
487
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
488
type='insert', line=util.html_clean(line[1:])))
490
elif line.startswith('-'):
491
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
492
type='delete', line=util.html_clean(line[1:])))
495
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
496
type='unknown', line=util.html_clean(repr(line))))
497
if chunk is not None:
501
def handle_modify(old_path, new_path, fid, kind):
503
modified.append(util.Container(filename=rich_filename(new_path, kind)))
505
old_lines = tree_lines(old_tree, fid)
506
new_lines = tree_lines(new_tree, fid)
508
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
509
diff = buffer.getvalue()
510
modified.append(util.Container(filename=rich_filename(new_path, kind), chunks=process_diff(diff)))
512
for path, fid, kind in delta.added:
513
added.append(rich_filename(path, kind))
515
for path, fid, kind, text_modified, meta_modified in delta.modified:
516
handle_modify(path, path, fid, kind)
518
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
519
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind)))
520
if meta_modified or text_modified:
521
handle_modify(oldpath, newpath, fid, kind)
523
for path, fid, kind in delta.removed:
524
removed.append(rich_filename(path, kind))
526
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
528
def get_filelist(self, inv, path):
530
return the list of all files (and their attributes) within a given
533
while path.endswith('/'):
535
if path.startswith('/'):
538
for filepath, entry in inv.entries():
539
if posixpath.dirname(filepath) != path:
541
filename = posixpath.basename(filepath)
542
rich_filename = filename
544
if entry.kind == 'directory':
548
revid = entry.revision
550
yield util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
551
pathname=pathname, revid=revid, revno=self.get_revno(revid), parity=parity)
555
def annotate_file(self, file_id, revid):
560
file_revid = self.get_inventory(revid)[file_id].revision
564
# because we cache revision metadata ourselves, it's actually much
565
# faster to call 'annotate_iter' on the weave directly than it is to
566
# ask bzrlib to annotate for us.
567
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
568
last_line_revid = None
569
for line_revid, text in w.annotate_iter(file_revid):
570
if line_revid == last_line_revid:
571
# remember which lines have a new revno and which don't
576
last_line_revid = line_revid
577
change = revision_cache.get(line_revid, None)
579
change = self.get_change(line_revid)
580
revision_cache[line_revid] = change
581
trunc_revno = change.revno
582
if len(trunc_revno) > 10:
583
trunc_revno = trunc_revno[:9] + '...'
585
yield util.Container(parity=parity, lineno=lineno, status=status,
586
trunc_revno=trunc_revno, change=change, text=util.html_clean(text))
589
log.debug('annotate: %r secs' % (time.time() - z,))
743
text_changes: list((filename, file_id)),
745
repo = self._branch.repository
746
if (bzrlib.revision.is_null(old_revid) or
747
bzrlib.revision.is_null(new_revid)):
748
old_tree, new_tree = map(
749
repo.revision_tree, [old_revid, new_revid])
751
old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
753
reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
755
bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
757
return util.Container(
758
added=sorted(reporter.added, key=lambda x: x.filename),
759
renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
760
removed=sorted(reporter.removed, key=lambda x: x.filename),
761
modified=sorted(reporter.modified, key=lambda x: x.filename),
762
text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))