Kiln » Kiln Storage Service Read More
Clone URL:  
annotationcache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# Copyright (C) 2010 - Anton Markov. All rights reserved. # (with code taken from mercurial/context.py by Matt Mackall) # # To enable the "cache-annotate" extension put these lines in your ~/.hgrc: # [extensions] # cache-annotate = /path/to/cache-annotate.py # # For help on the usage of cache-annotate see: # hg help cache-annotate # # For general help on the annotate command see: # hg help annotate # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. '''Caches the results of the annotate command This extension caches the results of the annotate command on every file and uses it to speed up the annotate command for child revisions of the file. Notes: 1. this version of annotate always follows copies and renames; use the --follow flag to display filenames in the annotations 2. use 'hg annotate --clear-cache' to remove all cached data 3. temporarily disable the extension with --no-cache Please see 'hg help annotate' for information about the annotate command. ''' import string import os import shutil from mercurial import bdiff, commands, extensions, store, util from mercurial.context import filectx from mercurial.node import nullrev from mercurial.i18n import _ CACHEPATH = 'annotations/' def hybridencode(f): return store._hybridencode(f, lambda path: store._auxencode(path, True)) class annotationcache(object): ''' Provides access to the cache of file annotations. The cache structure is /repo-path/.hg/annotations/path-to-file/rev[.{fn}] The cache is write-once so one does not worry about concurrent access. A cache file is line-oriented where each line is an n-tuple of strings separated by the separator character ':'. If the file has any ancestor with a different name, then we append .f or .n depending on whether or not we followed the annotation history to these ancestors. Otherwise a generic cache is created which works for either case. ''' def __init__(self, repo, follow = True): ''' Create a new annotations cache for the given repository ''' self.followflag = follow and 'f' or 'n' self._opener = repo.opener self.cachepath = repo.join("annotations") self.sepchar = ':' # fdcache caches information about existing files: # fdcache[path] is a file path or a file object if the file exists # Note: the file object must be open with mode "r" # fdcache[path] is False if the file does not exist # fdcache[path] does not exist if the file state is unknown self.fdcache = {} def opener(self, path, *args, **kwargs): return self._opener(CACHEPATH + path, *args, **kwargs) def makepath(self, filectx): ''' Computes the path to the cache for the given file revision. ''' relpath = os.path.join('data', filectx.path()) encrelpath = hybridencode(relpath) rev = str(filectx.rev()) return os.path.join(encrelpath, rev) def findcache(self, path): ''' Finds either the correct (follow or no-follow) cache or a general cache if the file has no renames ''' flaggedpath = path + '.' + self.followflag fullpath = os.path.join(self.cachepath, path) fullflaggedpath = os.path.join(self.cachepath, flaggedpath) return ((os.access(fullpath, os.F_OK) and path) or (os.access(fullflaggedpath, os.F_OK) and flaggedpath)) def __getitem__(self, filectx): ''' Returns the annotations for the given file revision. Throws IOError is the file does not exist ''' path = self.makepath(filectx) fullpath = None fd = None # if the file is in the cache, then either the file exists and # we know the correct or we have a file object open, # or the file does not exist and we can throw the appropriate # error right away without trying to open it if path in self.fdcache: if self.fdcache[path]: if isinstance(self.fdcache[path], file): fd = self.fdcache[path] else: fullpath = self.fdcache[path] else: raise IOError if not fullpath: fullpath = self.findcache(path) if not fd: fd = self.opener(fullpath, text=True, mode="r") self.fdcache[path] = fd return [tuple(string.split(line.strip(), self.sepchar)) for line in fd] def __contains__(self, filectx): ''' Checks for the existence of cached annotations for the given file revision without actually opening the file. ''' path = self.makepath(filectx) if not path in self.fdcache: self.fdcache[path] = self.findcache(path) return bool(self.fdcache[path]) def write(self, filectx, annotations, wasrenamed): ''' Caches the annotations for the given file revision. Fails silently if the file cannot be created. ''' path = self.makepath(filectx) try: if wasrenamed: path = self.makepath(filectx) + '.' + self.followflag else: path = self.makepath(filectx) fd = self.opener(path, text=True, mode="w") except IOError: return for annot in annotations: fd.write(string.join([str(f) for f in annot], self.sepchar) + os.linesep) fd.close() def clear(self): ''' Deletes all cached annotations ''' try: shutil.rmtree(self.cachepath) except (OSError,), err: if err.errno == os.errno.ENOENT: return else: raise util.Abort("Unable to clear the cache. " + "Try removing " + self.cachepath + " manually.") def cachedannotate(orig, self, follow=False, linenumber=None): '''This returns tuples of ((ctx, linenumber), line) for each line in the file, where ctx is the filectx of the node where that line was last changed and linenumber is the line number at the first appearance in the managed file. ''' def decorate(text, filectx): ''' Generate an annotation for every line in the file of the form (filepath, filerev, linenum, isrenamed, filectx) Note: this is a performance-critical function ''' size = len(text.splitlines()) path = filectx.path() rev = filectx.filerev() isrenamed = bool(filectx.renamed()) return ([(path, rev, i, isrenamed, filectx) for i in xrange(1, size + 1)], text) def pair(parent, child): ''' Merge the annotations from two revisions ''' for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]): child[0][b1:b2] = parent[0][a1:a2] return child getlog = util.lrucachefunc(lambda x: self._repo.file(x)) def getctx(path, fileid): ''' Retrieves a filectx for the given file and filerev ''' if path == self._path: log = self._filelog else: log = getlog(path) return filectx(self._repo, path, fileid=fileid, filelog=log) getctx = util.lrucachefunc(getctx) # Instantiate an annotation cache object to access the cached data acache = annotationcache(self._repo, follow) def parents(f): ''' Retrieves a filectx for every parent of the given revision ''' # we want to reuse filectx objects as much as possible p = f._path if f._filerev is None: # working dir pl = [(n.path(), n.filerev()) for n in f.parents()] else: pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)] # check if there are ancestors of different name if follow: r = f.renamed() if r: pl[0] = (r[0], getlog(r[0]).rev(r[1])) return [getctx(p, n) for p, n in pl if n != nullrev] # use linkrev to find the first changeset where self appeared if self.rev() != self.linkrev(): base = self.filectx(self.filerev()) else: base = self # find all ancestors needed = {base: 1} visit = [base] files = [base._path] while visit: f = visit.pop(0) # check for the the revision in the annotation cache # if the annotations are cached, stop searching that branch if f in acache: continue # if it was not found, we need to find all revisions that this one # depends on for p in parents(f): if p not in needed: needed[p] = 1 visit.append(p) if p._path not in files: files.append(p._path) else: # count how many times we'll use this needed[p] += 1 # sort by revision (per file) which is a topological order visit = [] for f in files: visit.extend(n for n in needed if n._path == f) # The history map contains the mapping from filectx to (decoration, text) # where decoration is (filepath, filerev, linenumber, filectx) # and filectx may be None if the annotations were cached hist = {} for f in sorted(visit, key=lambda x: x.rev()): # check for annotations in the cache and use cached if possible if f in acache: hist[f] = (acache[f], f.data()) continue curr = decorate(f.data(), f) for p in parents(f): # merge curr = pair(hist[p], curr) # trim the history of unneeded revs needed[p] -= 1 if not needed[p]: del hist[p] hist[f] = curr # Figure out if any entry was renamed # An entry is renamed if either: # (1) it comes from another name # (2) it is marked 'renamed' meaning it depends on another name def parsebool(s): return (s == True) or (s == 'True') isrenamed = any([(parsebool(entry[3]) or entry[0] != f.path()) for entry in hist[f][0]]) # Try to save the entries to the cache # It may fail silently and we don't care acache.write(f, [entry[0:4] for entry in hist[f][0]], isrenamed) def makefullannot(entry): ''' Instantiates missing filectx objects from (path, rev) pairs. ''' if len(entry) < 5: return (getctx(entry[0], entry[1]), entry[2]) else: return (entry[4], entry[2]) return zip([makefullannot(e) for e in hist[f][0]], hist[f][1].splitlines(True)) def clearcache(ui, repo): cache = annotationcache(repo) cache.clear() def uisetup(ui): ''' Setup the extension to handle annotations ''' def annotatewrapper(orig, ui, repo, *pats, **opts): if opts['clear_cache']: if not pats: clearcache(ui, repo) else: raise util.Abort("You cannot specify a file with --clear-cache") elif opts['no_cache']: orig(ui, repo, *pats, **opts) else: extensions.wrapfunction(filectx, 'annotate', cachedannotate) orig(ui, repo, *pats, **opts) entry = extensions.wrapcommand(commands.table, 'annotate', annotatewrapper) entry[1].extend([('', 'clear-cache', False, _('clear the annotations cache for all files')), ('', 'no-cache', False, _('temporarily disable caching'))])