##// END OF EJS Templates
verify: move checklog() onto class...
Durham Goode -
r27642:f6457349 default
parent child Browse files
Show More
@@ -1,363 +1,363 b''
1 # verify.py - repository integrity checking for Mercurial
1 # verify.py - repository integrity checking for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 nullid,
14 nullid,
15 short,
15 short,
16 )
16 )
17
17
18 from . import (
18 from . import (
19 error,
19 error,
20 revlog,
20 revlog,
21 util,
21 util,
22 )
22 )
23
23
24 def verify(repo):
24 def verify(repo):
25 lock = repo.lock()
25 lock = repo.lock()
26 try:
26 try:
27 return verifier(repo).verify()
27 return verifier(repo).verify()
28 finally:
28 finally:
29 lock.release()
29 lock.release()
30
30
31 def _normpath(f):
31 def _normpath(f):
32 # under hg < 2.4, convert didn't sanitize paths properly, so a
32 # under hg < 2.4, convert didn't sanitize paths properly, so a
33 # converted repo may contain repeated slashes
33 # converted repo may contain repeated slashes
34 while '//' in f:
34 while '//' in f:
35 f = f.replace('//', '/')
35 f = f.replace('//', '/')
36 return f
36 return f
37
37
38 def _validpath(repo, path):
38 def _validpath(repo, path):
39 """Returns False if a path should NOT be treated as part of a repo.
39 """Returns False if a path should NOT be treated as part of a repo.
40
40
41 For all in-core cases, this returns True, as we have no way for a
41 For all in-core cases, this returns True, as we have no way for a
42 path to be mentioned in the history but not actually be
42 path to be mentioned in the history but not actually be
43 relevant. For narrow clones, this is important because many
43 relevant. For narrow clones, this is important because many
44 filelogs will be missing, and changelog entries may mention
44 filelogs will be missing, and changelog entries may mention
45 modified files that are outside the narrow scope.
45 modified files that are outside the narrow scope.
46 """
46 """
47 return True
47 return True
48
48
49 class verifier(object):
49 class verifier(object):
50 def __init__(self, repo):
50 def __init__(self, repo):
51 self.repo = repo.unfiltered()
51 self.repo = repo.unfiltered()
52 self.ui = repo.ui
52 self.ui = repo.ui
53 self.badrevs = set()
53 self.badrevs = set()
54 self.errors = 0
54 self.errors = 0
55 self.warnings = 0
55 self.warnings = 0
56 self.havecl = len(repo.changelog) > 0
56 self.havecl = len(repo.changelog) > 0
57 self.havemf = len(repo.manifest) > 0
57 self.havemf = len(repo.manifest) > 0
58 self.revlogv1 = repo.changelog.version != revlog.REVLOGV0
58 self.revlogv1 = repo.changelog.version != revlog.REVLOGV0
59 self.lrugetctx = util.lrucachefunc(repo.changectx)
59 self.lrugetctx = util.lrucachefunc(repo.changectx)
60 self.refersmf = False
60 self.refersmf = False
61 self.fncachewarned = False
61 self.fncachewarned = False
62
62
63 def warn(self, msg):
63 def warn(self, msg):
64 self.ui.warn(msg + "\n")
64 self.ui.warn(msg + "\n")
65 self.warnings += 1
65 self.warnings += 1
66
66
67 def err(self, linkrev, msg, filename=None):
67 def err(self, linkrev, msg, filename=None):
68 if linkrev is not None:
68 if linkrev is not None:
69 self.badrevs.add(linkrev)
69 self.badrevs.add(linkrev)
70 else:
70 else:
71 linkrev = '?'
71 linkrev = '?'
72 msg = "%s: %s" % (linkrev, msg)
72 msg = "%s: %s" % (linkrev, msg)
73 if filename:
73 if filename:
74 msg = "%s@%s" % (filename, msg)
74 msg = "%s@%s" % (filename, msg)
75 self.ui.warn(" " + msg + "\n")
75 self.ui.warn(" " + msg + "\n")
76 self.errors += 1
76 self.errors += 1
77
77
78 def exc(self, linkrev, msg, inst, filename=None):
78 def exc(self, linkrev, msg, inst, filename=None):
79 if not str(inst):
79 if not str(inst):
80 inst = repr(inst)
80 inst = repr(inst)
81 self.err(linkrev, "%s: %s" % (msg, inst), filename)
81 self.err(linkrev, "%s: %s" % (msg, inst), filename)
82
82
83 def checklog(self, obj, name, linkrev):
84 if not len(obj) and (self.havecl or self.havemf):
85 self.err(linkrev, _("empty or missing %s") % name)
86 return
87
88 d = obj.checksize()
89 if d[0]:
90 self.err(None, _("data length off by %d bytes") % d[0], name)
91 if d[1]:
92 self.err(None, _("index contains %d extra bytes") % d[1], name)
93
94 if obj.version != revlog.REVLOGV0:
95 if not self.revlogv1:
96 self.warn(_("warning: `%s' uses revlog format 1") % name)
97 elif self.revlogv1:
98 self.warn(_("warning: `%s' uses revlog format 0") % name)
99
83 def verify(self):
100 def verify(self):
84 repo = self.repo
101 repo = self.repo
85 mflinkrevs = {}
102 mflinkrevs = {}
86 filelinkrevs = {}
103 filelinkrevs = {}
87 filenodes = {}
104 filenodes = {}
88 revisions = 0
105 revisions = 0
89 badrevs = self.badrevs
106 badrevs = self.badrevs
90 ui = repo.ui
107 ui = repo.ui
91 cl = repo.changelog
108 cl = repo.changelog
92 mf = repo.manifest
109 mf = repo.manifest
93 lrugetctx = self.lrugetctx
110 lrugetctx = self.lrugetctx
94
111
95 if not repo.url().startswith('file:'):
112 if not repo.url().startswith('file:'):
96 raise error.Abort(_("cannot verify bundle or remote repos"))
113 raise error.Abort(_("cannot verify bundle or remote repos"))
97
114
98 def checklog(obj, name, linkrev):
99 if not len(obj) and (havecl or havemf):
100 self.err(linkrev, _("empty or missing %s") % name)
101 return
102
103 d = obj.checksize()
104 if d[0]:
105 self.err(None, _("data length off by %d bytes") % d[0], name)
106 if d[1]:
107 self.err(None, _("index contains %d extra bytes") % d[1], name)
108
109 if obj.version != revlog.REVLOGV0:
110 if not revlogv1:
111 self.warn(_("warning: `%s' uses revlog format 1") % name)
112 elif revlogv1:
113 self.warn(_("warning: `%s' uses revlog format 0") % name)
114
115 def checkentry(obj, i, node, seen, linkrevs, f):
115 def checkentry(obj, i, node, seen, linkrevs, f):
116 lr = obj.linkrev(obj.rev(node))
116 lr = obj.linkrev(obj.rev(node))
117 if lr < 0 or (havecl and lr not in linkrevs):
117 if lr < 0 or (havecl and lr not in linkrevs):
118 if lr < 0 or lr >= len(cl):
118 if lr < 0 or lr >= len(cl):
119 msg = _("rev %d points to nonexistent changeset %d")
119 msg = _("rev %d points to nonexistent changeset %d")
120 else:
120 else:
121 msg = _("rev %d points to unexpected changeset %d")
121 msg = _("rev %d points to unexpected changeset %d")
122 self.err(None, msg % (i, lr), f)
122 self.err(None, msg % (i, lr), f)
123 if linkrevs:
123 if linkrevs:
124 if f and len(linkrevs) > 1:
124 if f and len(linkrevs) > 1:
125 try:
125 try:
126 # attempt to filter down to real linkrevs
126 # attempt to filter down to real linkrevs
127 linkrevs = [l for l in linkrevs
127 linkrevs = [l for l in linkrevs
128 if lrugetctx(l)[f].filenode() == node]
128 if lrugetctx(l)[f].filenode() == node]
129 except Exception:
129 except Exception:
130 pass
130 pass
131 self.warn(_(" (expected %s)") %
131 self.warn(_(" (expected %s)") %
132 " ".join(map(str, linkrevs)))
132 " ".join(map(str, linkrevs)))
133 lr = None # can't be trusted
133 lr = None # can't be trusted
134
134
135 try:
135 try:
136 p1, p2 = obj.parents(node)
136 p1, p2 = obj.parents(node)
137 if p1 not in seen and p1 != nullid:
137 if p1 not in seen and p1 != nullid:
138 self.err(lr, _("unknown parent 1 %s of %s") %
138 self.err(lr, _("unknown parent 1 %s of %s") %
139 (short(p1), short(node)), f)
139 (short(p1), short(node)), f)
140 if p2 not in seen and p2 != nullid:
140 if p2 not in seen and p2 != nullid:
141 self.err(lr, _("unknown parent 2 %s of %s") %
141 self.err(lr, _("unknown parent 2 %s of %s") %
142 (short(p2), short(node)), f)
142 (short(p2), short(node)), f)
143 except Exception as inst:
143 except Exception as inst:
144 self.exc(lr, _("checking parents of %s") % short(node), inst, f)
144 self.exc(lr, _("checking parents of %s") % short(node), inst, f)
145
145
146 if node in seen:
146 if node in seen:
147 self.err(lr, _("duplicate revision %d (%d)") %
147 self.err(lr, _("duplicate revision %d (%d)") %
148 (i, seen[node]), f)
148 (i, seen[node]), f)
149 seen[node] = i
149 seen[node] = i
150 return lr
150 return lr
151
151
152 if os.path.exists(repo.sjoin("journal")):
152 if os.path.exists(repo.sjoin("journal")):
153 ui.warn(_("abandoned transaction found - run hg recover\n"))
153 ui.warn(_("abandoned transaction found - run hg recover\n"))
154
154
155 revlogv1 = self.revlogv1
155 revlogv1 = self.revlogv1
156 if ui.verbose or not revlogv1:
156 if ui.verbose or not revlogv1:
157 ui.status(_("repository uses revlog format %d\n") %
157 ui.status(_("repository uses revlog format %d\n") %
158 (revlogv1 and 1 or 0))
158 (revlogv1 and 1 or 0))
159
159
160 havecl = self.havecl
160 havecl = self.havecl
161 havemf = self.havemf
161 havemf = self.havemf
162
162
163 ui.status(_("checking changesets\n"))
163 ui.status(_("checking changesets\n"))
164 seen = {}
164 seen = {}
165 checklog(cl, "changelog", 0)
165 self.checklog(cl, "changelog", 0)
166 total = len(repo)
166 total = len(repo)
167 for i in repo:
167 for i in repo:
168 ui.progress(_('checking'), i, total=total, unit=_('changesets'))
168 ui.progress(_('checking'), i, total=total, unit=_('changesets'))
169 n = cl.node(i)
169 n = cl.node(i)
170 checkentry(cl, i, n, seen, [i], "changelog")
170 checkentry(cl, i, n, seen, [i], "changelog")
171
171
172 try:
172 try:
173 changes = cl.read(n)
173 changes = cl.read(n)
174 if changes[0] != nullid:
174 if changes[0] != nullid:
175 mflinkrevs.setdefault(changes[0], []).append(i)
175 mflinkrevs.setdefault(changes[0], []).append(i)
176 self.refersmf = True
176 self.refersmf = True
177 for f in changes[3]:
177 for f in changes[3]:
178 if _validpath(repo, f):
178 if _validpath(repo, f):
179 filelinkrevs.setdefault(_normpath(f), []).append(i)
179 filelinkrevs.setdefault(_normpath(f), []).append(i)
180 except Exception as inst:
180 except Exception as inst:
181 self.refersmf = True
181 self.refersmf = True
182 self.exc(i, _("unpacking changeset %s") % short(n), inst)
182 self.exc(i, _("unpacking changeset %s") % short(n), inst)
183 ui.progress(_('checking'), None)
183 ui.progress(_('checking'), None)
184
184
185 ui.status(_("checking manifests\n"))
185 ui.status(_("checking manifests\n"))
186 seen = {}
186 seen = {}
187 if self.refersmf:
187 if self.refersmf:
188 # Do not check manifest if there are only changelog entries with
188 # Do not check manifest if there are only changelog entries with
189 # null manifests.
189 # null manifests.
190 checklog(mf, "manifest", 0)
190 self.checklog(mf, "manifest", 0)
191 total = len(mf)
191 total = len(mf)
192 for i in mf:
192 for i in mf:
193 ui.progress(_('checking'), i, total=total, unit=_('manifests'))
193 ui.progress(_('checking'), i, total=total, unit=_('manifests'))
194 n = mf.node(i)
194 n = mf.node(i)
195 lr = checkentry(mf, i, n, seen, mflinkrevs.get(n, []), "manifest")
195 lr = checkentry(mf, i, n, seen, mflinkrevs.get(n, []), "manifest")
196 if n in mflinkrevs:
196 if n in mflinkrevs:
197 del mflinkrevs[n]
197 del mflinkrevs[n]
198 else:
198 else:
199 self.err(lr, _("%s not in changesets") % short(n), "manifest")
199 self.err(lr, _("%s not in changesets") % short(n), "manifest")
200
200
201 try:
201 try:
202 for f, fn in mf.readdelta(n).iteritems():
202 for f, fn in mf.readdelta(n).iteritems():
203 if not f:
203 if not f:
204 self.err(lr, _("file without name in manifest"))
204 self.err(lr, _("file without name in manifest"))
205 elif f != "/dev/null": # ignore this in very old repos
205 elif f != "/dev/null": # ignore this in very old repos
206 if _validpath(repo, f):
206 if _validpath(repo, f):
207 filenodes.setdefault(
207 filenodes.setdefault(
208 _normpath(f), {}).setdefault(fn, lr)
208 _normpath(f), {}).setdefault(fn, lr)
209 except Exception as inst:
209 except Exception as inst:
210 self.exc(lr, _("reading manifest delta %s") % short(n), inst)
210 self.exc(lr, _("reading manifest delta %s") % short(n), inst)
211 ui.progress(_('checking'), None)
211 ui.progress(_('checking'), None)
212
212
213 ui.status(_("crosschecking files in changesets and manifests\n"))
213 ui.status(_("crosschecking files in changesets and manifests\n"))
214
214
215 total = len(mflinkrevs) + len(filelinkrevs) + len(filenodes)
215 total = len(mflinkrevs) + len(filelinkrevs) + len(filenodes)
216 count = 0
216 count = 0
217 if havemf:
217 if havemf:
218 for c, m in sorted([(c, m) for m in mflinkrevs
218 for c, m in sorted([(c, m) for m in mflinkrevs
219 for c in mflinkrevs[m]]):
219 for c in mflinkrevs[m]]):
220 count += 1
220 count += 1
221 if m == nullid:
221 if m == nullid:
222 continue
222 continue
223 ui.progress(_('crosschecking'), count, total=total)
223 ui.progress(_('crosschecking'), count, total=total)
224 self.err(c, _("changeset refers to unknown manifest %s") %
224 self.err(c, _("changeset refers to unknown manifest %s") %
225 short(m))
225 short(m))
226 mflinkrevs = None # del is bad here due to scope issues
226 mflinkrevs = None # del is bad here due to scope issues
227
227
228 for f in sorted(filelinkrevs):
228 for f in sorted(filelinkrevs):
229 count += 1
229 count += 1
230 ui.progress(_('crosschecking'), count, total=total)
230 ui.progress(_('crosschecking'), count, total=total)
231 if f not in filenodes:
231 if f not in filenodes:
232 lr = filelinkrevs[f][0]
232 lr = filelinkrevs[f][0]
233 self.err(lr, _("in changeset but not in manifest"), f)
233 self.err(lr, _("in changeset but not in manifest"), f)
234
234
235 if havecl:
235 if havecl:
236 for f in sorted(filenodes):
236 for f in sorted(filenodes):
237 count += 1
237 count += 1
238 ui.progress(_('crosschecking'), count, total=total)
238 ui.progress(_('crosschecking'), count, total=total)
239 if f not in filelinkrevs:
239 if f not in filelinkrevs:
240 try:
240 try:
241 fl = repo.file(f)
241 fl = repo.file(f)
242 lr = min([fl.linkrev(fl.rev(n)) for n in filenodes[f]])
242 lr = min([fl.linkrev(fl.rev(n)) for n in filenodes[f]])
243 except Exception:
243 except Exception:
244 lr = None
244 lr = None
245 self.err(lr, _("in manifest but not in changeset"), f)
245 self.err(lr, _("in manifest but not in changeset"), f)
246
246
247 ui.progress(_('crosschecking'), None)
247 ui.progress(_('crosschecking'), None)
248
248
249 ui.status(_("checking files\n"))
249 ui.status(_("checking files\n"))
250
250
251 storefiles = set()
251 storefiles = set()
252 for f, f2, size in repo.store.datafiles():
252 for f, f2, size in repo.store.datafiles():
253 if not f:
253 if not f:
254 self.err(None, _("cannot decode filename '%s'") % f2)
254 self.err(None, _("cannot decode filename '%s'") % f2)
255 elif size > 0 or not revlogv1:
255 elif size > 0 or not revlogv1:
256 storefiles.add(_normpath(f))
256 storefiles.add(_normpath(f))
257
257
258 files = sorted(set(filenodes) | set(filelinkrevs))
258 files = sorted(set(filenodes) | set(filelinkrevs))
259 total = len(files)
259 total = len(files)
260 for i, f in enumerate(files):
260 for i, f in enumerate(files):
261 ui.progress(_('checking'), i, item=f, total=total)
261 ui.progress(_('checking'), i, item=f, total=total)
262 try:
262 try:
263 linkrevs = filelinkrevs[f]
263 linkrevs = filelinkrevs[f]
264 except KeyError:
264 except KeyError:
265 # in manifest but not in changelog
265 # in manifest but not in changelog
266 linkrevs = []
266 linkrevs = []
267
267
268 if linkrevs:
268 if linkrevs:
269 lr = linkrevs[0]
269 lr = linkrevs[0]
270 else:
270 else:
271 lr = None
271 lr = None
272
272
273 try:
273 try:
274 fl = repo.file(f)
274 fl = repo.file(f)
275 except error.RevlogError as e:
275 except error.RevlogError as e:
276 self.err(lr, _("broken revlog! (%s)") % e, f)
276 self.err(lr, _("broken revlog! (%s)") % e, f)
277 continue
277 continue
278
278
279 for ff in fl.files():
279 for ff in fl.files():
280 try:
280 try:
281 storefiles.remove(ff)
281 storefiles.remove(ff)
282 except KeyError:
282 except KeyError:
283 self.warn(_(" warning: revlog '%s' not in fncache!") % ff)
283 self.warn(_(" warning: revlog '%s' not in fncache!") % ff)
284 self.fncachewarned = True
284 self.fncachewarned = True
285
285
286 checklog(fl, f, lr)
286 self.checklog(fl, f, lr)
287 seen = {}
287 seen = {}
288 rp = None
288 rp = None
289 for i in fl:
289 for i in fl:
290 revisions += 1
290 revisions += 1
291 n = fl.node(i)
291 n = fl.node(i)
292 lr = checkentry(fl, i, n, seen, linkrevs, f)
292 lr = checkentry(fl, i, n, seen, linkrevs, f)
293 if f in filenodes:
293 if f in filenodes:
294 if havemf and n not in filenodes[f]:
294 if havemf and n not in filenodes[f]:
295 self.err(lr, _("%s not in manifests") % (short(n)), f)
295 self.err(lr, _("%s not in manifests") % (short(n)), f)
296 else:
296 else:
297 del filenodes[f][n]
297 del filenodes[f][n]
298
298
299 # verify contents
299 # verify contents
300 try:
300 try:
301 l = len(fl.read(n))
301 l = len(fl.read(n))
302 rp = fl.renamed(n)
302 rp = fl.renamed(n)
303 if l != fl.size(i):
303 if l != fl.size(i):
304 if len(fl.revision(n)) != fl.size(i):
304 if len(fl.revision(n)) != fl.size(i):
305 self.err(lr, _("unpacked size is %s, %s expected") %
305 self.err(lr, _("unpacked size is %s, %s expected") %
306 (l, fl.size(i)), f)
306 (l, fl.size(i)), f)
307 except error.CensoredNodeError:
307 except error.CensoredNodeError:
308 # experimental config: censor.policy
308 # experimental config: censor.policy
309 if ui.config("censor", "policy", "abort") == "abort":
309 if ui.config("censor", "policy", "abort") == "abort":
310 self.err(lr, _("censored file data"), f)
310 self.err(lr, _("censored file data"), f)
311 except Exception as inst:
311 except Exception as inst:
312 self.exc(lr, _("unpacking %s") % short(n), inst, f)
312 self.exc(lr, _("unpacking %s") % short(n), inst, f)
313
313
314 # check renames
314 # check renames
315 try:
315 try:
316 if rp:
316 if rp:
317 if lr is not None and ui.verbose:
317 if lr is not None and ui.verbose:
318 ctx = lrugetctx(lr)
318 ctx = lrugetctx(lr)
319 found = False
319 found = False
320 for pctx in ctx.parents():
320 for pctx in ctx.parents():
321 if rp[0] in pctx:
321 if rp[0] in pctx:
322 found = True
322 found = True
323 break
323 break
324 if not found:
324 if not found:
325 self.warn(_("warning: copy source of '%s' not"
325 self.warn(_("warning: copy source of '%s' not"
326 " in parents of %s") % (f, ctx))
326 " in parents of %s") % (f, ctx))
327 fl2 = repo.file(rp[0])
327 fl2 = repo.file(rp[0])
328 if not len(fl2):
328 if not len(fl2):
329 self.err(lr, _("empty or missing copy source "
329 self.err(lr, _("empty or missing copy source "
330 "revlog %s:%s") % (rp[0], short(rp[1])), f)
330 "revlog %s:%s") % (rp[0], short(rp[1])), f)
331 elif rp[1] == nullid:
331 elif rp[1] == nullid:
332 ui.note(_("warning: %s@%s: copy source"
332 ui.note(_("warning: %s@%s: copy source"
333 " revision is nullid %s:%s\n")
333 " revision is nullid %s:%s\n")
334 % (f, lr, rp[0], short(rp[1])))
334 % (f, lr, rp[0], short(rp[1])))
335 else:
335 else:
336 fl2.rev(rp[1])
336 fl2.rev(rp[1])
337 except Exception as inst:
337 except Exception as inst:
338 self.exc(lr, _("checking rename of %s") % short(n), inst, f)
338 self.exc(lr, _("checking rename of %s") % short(n), inst, f)
339
339
340 # cross-check
340 # cross-check
341 if f in filenodes:
341 if f in filenodes:
342 fns = [(lr, n) for n, lr in filenodes[f].iteritems()]
342 fns = [(lr, n) for n, lr in filenodes[f].iteritems()]
343 for lr, node in sorted(fns):
343 for lr, node in sorted(fns):
344 self.err(lr, _("%s in manifests not found") % short(node),
344 self.err(lr, _("%s in manifests not found") % short(node),
345 f)
345 f)
346 ui.progress(_('checking'), None)
346 ui.progress(_('checking'), None)
347
347
348 for f in storefiles:
348 for f in storefiles:
349 self.warn(_("warning: orphan revlog '%s'") % f)
349 self.warn(_("warning: orphan revlog '%s'") % f)
350
350
351 ui.status(_("%d files, %d changesets, %d total revisions\n") %
351 ui.status(_("%d files, %d changesets, %d total revisions\n") %
352 (len(files), len(cl), revisions))
352 (len(files), len(cl), revisions))
353 if self.warnings:
353 if self.warnings:
354 ui.warn(_("%d warnings encountered!\n") % self.warnings)
354 ui.warn(_("%d warnings encountered!\n") % self.warnings)
355 if self.fncachewarned:
355 if self.fncachewarned:
356 ui.warn(_('hint: run "hg debugrebuildfncache" to recover from '
356 ui.warn(_('hint: run "hg debugrebuildfncache" to recover from '
357 'corrupt fncache\n'))
357 'corrupt fncache\n'))
358 if self.errors:
358 if self.errors:
359 ui.warn(_("%d integrity errors encountered!\n") % self.errors)
359 ui.warn(_("%d integrity errors encountered!\n") % self.errors)
360 if badrevs:
360 if badrevs:
361 ui.warn(_("(first damaged changeset appears to be %d)\n")
361 ui.warn(_("(first damaged changeset appears to be %d)\n")
362 % min(badrevs))
362 % min(badrevs))
363 return 1
363 return 1
General Comments 0
You need to be logged in to leave comments. Login now