##// END OF EJS Templates
verify: explicitly return 0 if no error are encountered...
marmoute -
r42034:567892b4 default
parent child Browse files
Show More
@@ -1,466 +1,467
1 1 # verify.py - repository integrity checking for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 nullid,
15 15 short,
16 16 )
17 17
18 18 from . import (
19 19 error,
20 20 pycompat,
21 21 revlog,
22 22 util,
23 23 )
24 24
25 25 def verify(repo):
26 26 with repo.lock():
27 27 return verifier(repo).verify()
28 28
29 29 def _normpath(f):
30 30 # under hg < 2.4, convert didn't sanitize paths properly, so a
31 31 # converted repo may contain repeated slashes
32 32 while '//' in f:
33 33 f = f.replace('//', '/')
34 34 return f
35 35
36 36 class verifier(object):
37 37 def __init__(self, repo):
38 38 self.repo = repo.unfiltered()
39 39 self.ui = repo.ui
40 40 self.match = repo.narrowmatch()
41 41 self.badrevs = set()
42 42 self.errors = 0
43 43 self.warnings = 0
44 44 self.havecl = len(repo.changelog) > 0
45 45 self.havemf = len(repo.manifestlog.getstorage(b'')) > 0
46 46 self.revlogv1 = repo.changelog.version != revlog.REVLOGV0
47 47 self.lrugetctx = util.lrucachefunc(repo.__getitem__)
48 48 self.refersmf = False
49 49 self.fncachewarned = False
50 50 # developer config: verify.skipflags
51 51 self.skipflags = repo.ui.configint('verify', 'skipflags')
52 52 self.warnorphanstorefiles = True
53 53
54 54 def _warn(self, msg):
55 55 """record a "warning" level issue"""
56 56 self.ui.warn(msg + "\n")
57 57 self.warnings += 1
58 58
59 59 def _err(self, linkrev, msg, filename=None):
60 60 """record a "error" level issue"""
61 61 if linkrev is not None:
62 62 self.badrevs.add(linkrev)
63 63 linkrev = "%d" % linkrev
64 64 else:
65 65 linkrev = '?'
66 66 msg = "%s: %s" % (linkrev, msg)
67 67 if filename:
68 68 msg = "%s@%s" % (filename, msg)
69 69 self.ui.warn(" " + msg + "\n")
70 70 self.errors += 1
71 71
72 72 def _exc(self, linkrev, msg, inst, filename=None):
73 73 """record exception raised during the verify process"""
74 74 fmsg = pycompat.bytestr(inst)
75 75 if not fmsg:
76 76 fmsg = pycompat.byterepr(inst)
77 77 self._err(linkrev, "%s: %s" % (msg, fmsg), filename)
78 78
79 79 def checklog(self, obj, name, linkrev):
80 80 if not len(obj) and (self.havecl or self.havemf):
81 81 self._err(linkrev, _("empty or missing %s") % name)
82 82 return
83 83
84 84 d = obj.checksize()
85 85 if d[0]:
86 86 self.err(None, _("data length off by %d bytes") % d[0], name)
87 87 if d[1]:
88 88 self.err(None, _("index contains %d extra bytes") % d[1], name)
89 89
90 90 if obj.version != revlog.REVLOGV0:
91 91 if not self.revlogv1:
92 92 self._warn(_("warning: `%s' uses revlog format 1") % name)
93 93 elif self.revlogv1:
94 94 self._warn(_("warning: `%s' uses revlog format 0") % name)
95 95
96 96 def checkentry(self, obj, i, node, seen, linkrevs, f):
97 97 lr = obj.linkrev(obj.rev(node))
98 98 if lr < 0 or (self.havecl and lr not in linkrevs):
99 99 if lr < 0 or lr >= len(self.repo.changelog):
100 100 msg = _("rev %d points to nonexistent changeset %d")
101 101 else:
102 102 msg = _("rev %d points to unexpected changeset %d")
103 103 self._err(None, msg % (i, lr), f)
104 104 if linkrevs:
105 105 if f and len(linkrevs) > 1:
106 106 try:
107 107 # attempt to filter down to real linkrevs
108 108 linkrevs = [l for l in linkrevs
109 109 if self.lrugetctx(l)[f].filenode() == node]
110 110 except Exception:
111 111 pass
112 112 self._warn(_(" (expected %s)") % " ".join
113 113 (map(pycompat.bytestr, linkrevs)))
114 114 lr = None # can't be trusted
115 115
116 116 try:
117 117 p1, p2 = obj.parents(node)
118 118 if p1 not in seen and p1 != nullid:
119 119 self._err(lr, _("unknown parent 1 %s of %s") %
120 120 (short(p1), short(node)), f)
121 121 if p2 not in seen and p2 != nullid:
122 122 self._err(lr, _("unknown parent 2 %s of %s") %
123 123 (short(p2), short(node)), f)
124 124 except Exception as inst:
125 125 self._exc(lr, _("checking parents of %s") % short(node), inst, f)
126 126
127 127 if node in seen:
128 128 self._err(lr, _("duplicate revision %d (%d)") % (i, seen[node]), f)
129 129 seen[node] = i
130 130 return lr
131 131
132 132 def verify(self):
133 133 """verify the content of the Mercurial repository
134 134
135 135 This method run all verifications, displaying issues as they are found.
136 136
137 return 1 if any error have been encountered"""
137 return 1 if any error have been encountered, 0 otherwise."""
138 138 repo = self.repo
139 139
140 140 ui = repo.ui
141 141
142 142 if not repo.url().startswith('file:'):
143 143 raise error.Abort(_("cannot verify bundle or remote repos"))
144 144
145 145 if os.path.exists(repo.sjoin("journal")):
146 146 ui.warn(_("abandoned transaction found - run hg recover\n"))
147 147
148 148 if ui.verbose or not self.revlogv1:
149 149 ui.status(_("repository uses revlog format %d\n") %
150 150 (self.revlogv1 and 1 or 0))
151 151
152 152 mflinkrevs, filelinkrevs = self._verifychangelog()
153 153
154 154 filenodes = self._verifymanifest(mflinkrevs)
155 155 del mflinkrevs
156 156
157 157 self._crosscheckfiles(filelinkrevs, filenodes)
158 158
159 159 totalfiles, filerevisions = self._verifyfiles(filenodes, filelinkrevs)
160 160
161 161 ui.status(_("checked %d changesets with %d changes to %d files\n") %
162 162 (len(repo.changelog), filerevisions, totalfiles))
163 163 if self.warnings:
164 164 ui.warn(_("%d warnings encountered!\n") % self.warnings)
165 165 if self.fncachewarned:
166 166 ui.warn(_('hint: run "hg debugrebuildfncache" to recover from '
167 167 'corrupt fncache\n'))
168 168 if self.errors:
169 169 ui.warn(_("%d integrity errors encountered!\n") % self.errors)
170 170 if self.badrevs:
171 171 ui.warn(_("(first damaged changeset appears to be %d)\n")
172 172 % min(self.badrevs))
173 173 return 1
174 return 0
174 175
175 176 def _verifychangelog(self):
176 177 ui = self.ui
177 178 repo = self.repo
178 179 match = self.match
179 180 cl = repo.changelog
180 181
181 182 ui.status(_("checking changesets\n"))
182 183 mflinkrevs = {}
183 184 filelinkrevs = {}
184 185 seen = {}
185 186 self.checklog(cl, "changelog", 0)
186 187 progress = ui.makeprogress(_('checking'), unit=_('changesets'),
187 188 total=len(repo))
188 189 for i in repo:
189 190 progress.update(i)
190 191 n = cl.node(i)
191 192 self.checkentry(cl, i, n, seen, [i], "changelog")
192 193
193 194 try:
194 195 changes = cl.read(n)
195 196 if changes[0] != nullid:
196 197 mflinkrevs.setdefault(changes[0], []).append(i)
197 198 self.refersmf = True
198 199 for f in changes[3]:
199 200 if match(f):
200 201 filelinkrevs.setdefault(_normpath(f), []).append(i)
201 202 except Exception as inst:
202 203 self.refersmf = True
203 204 self._exc(i, _("unpacking changeset %s") % short(n), inst)
204 205 progress.complete()
205 206 return mflinkrevs, filelinkrevs
206 207
207 208 def _verifymanifest(self, mflinkrevs, dir="", storefiles=None,
208 209 subdirprogress=None):
209 210 repo = self.repo
210 211 ui = self.ui
211 212 match = self.match
212 213 mfl = self.repo.manifestlog
213 214 mf = mfl.getstorage(dir)
214 215
215 216 if not dir:
216 217 self.ui.status(_("checking manifests\n"))
217 218
218 219 filenodes = {}
219 220 subdirnodes = {}
220 221 seen = {}
221 222 label = "manifest"
222 223 if dir:
223 224 label = dir
224 225 revlogfiles = mf.files()
225 226 storefiles.difference_update(revlogfiles)
226 227 if subdirprogress: # should be true since we're in a subdirectory
227 228 subdirprogress.increment()
228 229 if self.refersmf:
229 230 # Do not check manifest if there are only changelog entries with
230 231 # null manifests.
231 232 self.checklog(mf, label, 0)
232 233 progress = ui.makeprogress(_('checking'), unit=_('manifests'),
233 234 total=len(mf))
234 235 for i in mf:
235 236 if not dir:
236 237 progress.update(i)
237 238 n = mf.node(i)
238 239 lr = self.checkentry(mf, i, n, seen, mflinkrevs.get(n, []), label)
239 240 if n in mflinkrevs:
240 241 del mflinkrevs[n]
241 242 elif dir:
242 243 self._err(lr, _("%s not in parent-directory manifest") %
243 244 short(n), label)
244 245 else:
245 246 self._err(lr, _("%s not in changesets") % short(n), label)
246 247
247 248 try:
248 249 mfdelta = mfl.get(dir, n).readdelta(shallow=True)
249 250 for f, fn, fl in mfdelta.iterentries():
250 251 if not f:
251 252 self._err(lr, _("entry without name in manifest"))
252 253 elif f == "/dev/null": # ignore this in very old repos
253 254 continue
254 255 fullpath = dir + _normpath(f)
255 256 if fl == 't':
256 257 if not match.visitdir(fullpath):
257 258 continue
258 259 subdirnodes.setdefault(fullpath + '/', {}).setdefault(
259 260 fn, []).append(lr)
260 261 else:
261 262 if not match(fullpath):
262 263 continue
263 264 filenodes.setdefault(fullpath, {}).setdefault(fn, lr)
264 265 except Exception as inst:
265 266 self._exc(lr, _("reading delta %s") % short(n), inst, label)
266 267 if not dir:
267 268 progress.complete()
268 269
269 270 if self.havemf:
270 271 for c, m in sorted([(c, m) for m in mflinkrevs
271 272 for c in mflinkrevs[m]]):
272 273 if dir:
273 274 self._err(c, _("parent-directory manifest refers to unknown"
274 275 " revision %s") % short(m), label)
275 276 else:
276 277 self._err(c, _("changeset refers to unknown revision %s") %
277 278 short(m), label)
278 279
279 280 if not dir and subdirnodes:
280 281 self.ui.status(_("checking directory manifests\n"))
281 282 storefiles = set()
282 283 subdirs = set()
283 284 revlogv1 = self.revlogv1
284 285 for f, f2, size in repo.store.datafiles():
285 286 if not f:
286 287 self._err(None, _("cannot decode filename '%s'") % f2)
287 288 elif (size > 0 or not revlogv1) and f.startswith('meta/'):
288 289 storefiles.add(_normpath(f))
289 290 subdirs.add(os.path.dirname(f))
290 291 subdirprogress = ui.makeprogress(_('checking'), unit=_('manifests'),
291 292 total=len(subdirs))
292 293
293 294 for subdir, linkrevs in subdirnodes.iteritems():
294 295 subdirfilenodes = self._verifymanifest(linkrevs, subdir, storefiles,
295 296 subdirprogress)
296 297 for f, onefilenodes in subdirfilenodes.iteritems():
297 298 filenodes.setdefault(f, {}).update(onefilenodes)
298 299
299 300 if not dir and subdirnodes:
300 301 subdirprogress.complete()
301 302 if self.warnorphanstorefiles:
302 303 for f in sorted(storefiles):
303 304 self._warn(_("warning: orphan data file '%s'") % f)
304 305
305 306 return filenodes
306 307
307 308 def _crosscheckfiles(self, filelinkrevs, filenodes):
308 309 repo = self.repo
309 310 ui = self.ui
310 311 ui.status(_("crosschecking files in changesets and manifests\n"))
311 312
312 313 total = len(filelinkrevs) + len(filenodes)
313 314 progress = ui.makeprogress(_('crosschecking'), unit=_('files'),
314 315 total=total)
315 316 if self.havemf:
316 317 for f in sorted(filelinkrevs):
317 318 progress.increment()
318 319 if f not in filenodes:
319 320 lr = filelinkrevs[f][0]
320 321 self._err(lr, _("in changeset but not in manifest"), f)
321 322
322 323 if self.havecl:
323 324 for f in sorted(filenodes):
324 325 progress.increment()
325 326 if f not in filelinkrevs:
326 327 try:
327 328 fl = repo.file(f)
328 329 lr = min([fl.linkrev(fl.rev(n)) for n in filenodes[f]])
329 330 except Exception:
330 331 lr = None
331 332 self._err(lr, _("in manifest but not in changeset"), f)
332 333
333 334 progress.complete()
334 335
335 336 def _verifyfiles(self, filenodes, filelinkrevs):
336 337 repo = self.repo
337 338 ui = self.ui
338 339 lrugetctx = self.lrugetctx
339 340 revlogv1 = self.revlogv1
340 341 havemf = self.havemf
341 342 ui.status(_("checking files\n"))
342 343
343 344 storefiles = set()
344 345 for f, f2, size in repo.store.datafiles():
345 346 if not f:
346 347 self._err(None, _("cannot decode filename '%s'") % f2)
347 348 elif (size > 0 or not revlogv1) and f.startswith('data/'):
348 349 storefiles.add(_normpath(f))
349 350
350 351 state = {
351 352 # TODO this assumes revlog storage for changelog.
352 353 'expectedversion': self.repo.changelog.version & 0xFFFF,
353 354 'skipflags': self.skipflags,
354 355 # experimental config: censor.policy
355 356 'erroroncensored': ui.config('censor', 'policy') == 'abort',
356 357 }
357 358
358 359 files = sorted(set(filenodes) | set(filelinkrevs))
359 360 revisions = 0
360 361 progress = ui.makeprogress(_('checking'), unit=_('files'),
361 362 total=len(files))
362 363 for i, f in enumerate(files):
363 364 progress.update(i, item=f)
364 365 try:
365 366 linkrevs = filelinkrevs[f]
366 367 except KeyError:
367 368 # in manifest but not in changelog
368 369 linkrevs = []
369 370
370 371 if linkrevs:
371 372 lr = linkrevs[0]
372 373 else:
373 374 lr = None
374 375
375 376 try:
376 377 fl = repo.file(f)
377 378 except error.StorageError as e:
378 379 self._err(lr, _("broken revlog! (%s)") % e, f)
379 380 continue
380 381
381 382 for ff in fl.files():
382 383 try:
383 384 storefiles.remove(ff)
384 385 except KeyError:
385 386 if self.warnorphanstorefiles:
386 387 self._warn(_(" warning: revlog '%s' not in fncache!") %
387 388 ff)
388 389 self.fncachewarned = True
389 390
390 391 if not len(fl) and (self.havecl or self.havemf):
391 392 self._err(lr, _("empty or missing %s") % f)
392 393 else:
393 394 # Guard against implementations not setting this.
394 395 state['skipread'] = set()
395 396 for problem in fl.verifyintegrity(state):
396 397 if problem.node is not None:
397 398 linkrev = fl.linkrev(fl.rev(problem.node))
398 399 else:
399 400 linkrev = None
400 401
401 402 if problem.warning:
402 403 self._warn(problem.warning)
403 404 elif problem.error:
404 405 self._err(linkrev if linkrev is not None else lr,
405 406 problem.error, f)
406 407 else:
407 408 raise error.ProgrammingError(
408 409 'problem instance does not set warning or error '
409 410 'attribute: %s' % problem.msg)
410 411
411 412 seen = {}
412 413 for i in fl:
413 414 revisions += 1
414 415 n = fl.node(i)
415 416 lr = self.checkentry(fl, i, n, seen, linkrevs, f)
416 417 if f in filenodes:
417 418 if havemf and n not in filenodes[f]:
418 419 self._err(lr, _("%s not in manifests") % (short(n)), f)
419 420 else:
420 421 del filenodes[f][n]
421 422
422 423 if n in state['skipread']:
423 424 continue
424 425
425 426 # check renames
426 427 try:
427 428 # This requires resolving fulltext (at least on revlogs). We
428 429 # may want ``verifyintegrity()`` to pass a set of nodes with
429 430 # rename metadata as an optimization.
430 431 rp = fl.renamed(n)
431 432 if rp:
432 433 if lr is not None and ui.verbose:
433 434 ctx = lrugetctx(lr)
434 435 if not any(rp[0] in pctx for pctx in ctx.parents()):
435 436 self._warn(_("warning: copy source of '%s' not"
436 437 " in parents of %s") % (f, ctx))
437 438 fl2 = repo.file(rp[0])
438 439 if not len(fl2):
439 440 self._err(lr,
440 441 _("empty or missing copy source revlog "
441 442 "%s:%s") % (rp[0],
442 443 short(rp[1])),
443 444 f)
444 445 elif rp[1] == nullid:
445 446 ui.note(_("warning: %s@%s: copy source"
446 447 " revision is nullid %s:%s\n")
447 448 % (f, lr, rp[0], short(rp[1])))
448 449 else:
449 450 fl2.rev(rp[1])
450 451 except Exception as inst:
451 452 self._exc(lr, _("checking rename of %s") % short(n),
452 453 inst, f)
453 454
454 455 # cross-check
455 456 if f in filenodes:
456 457 fns = [(v, k) for k, v in filenodes[f].iteritems()]
457 458 for lr, node in sorted(fns):
458 459 self._err(lr, _("manifest refers to unknown revision %s") %
459 460 short(node), f)
460 461 progress.complete()
461 462
462 463 if self.warnorphanstorefiles:
463 464 for f in sorted(storefiles):
464 465 self._warn(_("warning: orphan data file '%s'") % f)
465 466
466 467 return len(files), revisions
General Comments 0
You need to be logged in to leave comments. Login now