##// END OF EJS Templates
admin-command: add verify command...
Raphaël Gomès -
r51882:752c5a5b default
parent child Browse files
Show More
@@ -0,0 +1,341 b''
1 # admin/verify.py - better repository integrity checking for Mercurial
2 #
3 # Copyright 2023 Octobus <contact@octobus.net>
4 #
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.
7
8 import collections
9 import copy
10 import functools
11
12 from ..i18n import _
13 from .. import error, pycompat, registrar, requirements
14 from ..utils import stringutil
15
16
17 verify_table = {}
18 verify_alias_table = {}
19 check = registrar.verify_check(verify_table, verify_alias_table)
20
21
22 # Use this to declare options/aliases in the middle of the hierarchy.
23 # Checks like these are not run themselves and cannot have a body.
24 # For an example, see the `revlogs` check.
25 def noop_func(*args, **kwargs):
26 return
27
28
29 @check(b"working-copy.dirstate", alias=b"dirstate")
30 def check_dirstate(ui, repo, **options):
31 ui.status(_(b"checking dirstate\n"))
32
33 parent1, parent2 = repo.dirstate.parents()
34 m1 = repo[parent1].manifest()
35 m2 = repo[parent2].manifest()
36 errors = 0
37
38 is_narrow = requirements.NARROW_REQUIREMENT in repo.requirements
39 narrow_matcher = repo.narrowmatch() if is_narrow else None
40
41 for err in repo.dirstate.verify(m1, m2, narrow_matcher):
42 ui.warn(err[0] % err[1:])
43 errors += 1
44
45 return errors
46
47
48 # Tree of all checks and their associated function
49 pyramid = {}
50
51
52 def build_pyramid(table, full_pyramid):
53 """Create a pyramid of checks of the registered checks.
54 It is a name-based hierarchy that can be arbitrarily nested."""
55 for entry, func in sorted(table.items(), key=lambda x: x[0], reverse=True):
56 cursor = full_pyramid
57 levels = entry.split(b".")
58 for level in levels[:-1]:
59 current_node = cursor.setdefault(level, {})
60 cursor = current_node
61 if cursor.get(levels[-1]) is None:
62 cursor[levels[-1]] = (entry, func)
63 elif func is not noop_func:
64 m = b"intermediate checks need to use `verify.noop_func`"
65 raise error.ProgrammingError(m)
66
67
68 def find_checks(name, table=None, alias_table=None, full_pyramid=None):
69 """Find all checks for a given name and returns a dict of
70 (qualified_check_name, check_function)
71
72 # Examples
73
74 Using a full qualified name:
75 "working-copy.dirstate" -> {
76 "working-copy.dirstate": CF,
77 }
78
79 Using a *prefix* of a qualified name:
80 "store.revlogs" -> {
81 "store.revlogs.changelog": CF,
82 "store.revlogs.manifestlog": CF,
83 "store.revlogs.filelog": CF,
84 }
85
86 Using a defined alias:
87 "revlogs" -> {
88 "store.revlogs.changelog": CF,
89 "store.revlogs.manifestlog": CF,
90 "store.revlogs.filelog": CF,
91 }
92
93 Using something that is none of the above will be an error.
94 """
95 if table is None:
96 table = verify_table
97 if alias_table is None:
98 alias_table = verify_alias_table
99
100 if name == b"full":
101 return table
102 checks = {}
103
104 # is it a full name?
105 check = table.get(name)
106
107 if check is None:
108 # is it an alias?
109 qualified_name = alias_table.get(name)
110 if qualified_name is not None:
111 name = qualified_name
112 check = table.get(name)
113 else:
114 split = name.split(b".", 1)
115 if len(split) == 2:
116 # split[0] can be an alias
117 qualified_name = alias_table.get(split[0])
118 if qualified_name is not None:
119 name = b"%s.%s" % (qualified_name, split[1])
120 check = table.get(name)
121 else:
122 qualified_name = name
123
124 # Maybe it's a subtree in the check hierarchy that does not
125 # have an explicit alias.
126 levels = name.split(b".")
127 if full_pyramid is not None:
128 if not full_pyramid:
129 build_pyramid(table, full_pyramid)
130
131 pyramid.clear()
132 pyramid.update(full_pyramid.items())
133 else:
134 build_pyramid(table, pyramid)
135
136 subtree = pyramid
137 # Find subtree
138 for level in levels:
139 subtree = subtree.get(level)
140 if subtree is None:
141 hint = error.getsimilar(list(alias_table) + list(table), name)
142 hint = error.similarity_hint(hint)
143
144 raise error.InputError(_(b"unknown check %s" % name), hint=hint)
145
146 # Get all checks in that subtree
147 if isinstance(subtree, dict):
148 stack = list(subtree.items())
149 while stack:
150 current_name, entry = stack.pop()
151 if isinstance(entry, dict):
152 stack.extend(entry.items())
153 else:
154 # (qualified_name, func)
155 checks[entry[0]] = entry[1]
156 else:
157 checks[name] = check
158
159 return checks
160
161
162 def pass_options(
163 ui,
164 checks,
165 options,
166 table=None,
167 alias_table=None,
168 full_pyramid=None,
169 ):
170 """Given a dict of checks (fully qualified name to function), and a list
171 of options as given by the user, pass each option down to the right check
172 function."""
173 ui.debug(b"passing options to check functions\n")
174 to_modify = collections.defaultdict(dict)
175
176 if not checks:
177 raise error.Error(_(b"`checks` required"))
178
179 for option in sorted(options):
180 split = option.split(b":")
181 hint = _(
182 b"syntax is 'check:option=value', "
183 b"eg. revlogs.changelog:copies=yes"
184 )
185 option_error = error.InputError(
186 _(b"invalid option '%s'") % option, hint=hint
187 )
188 if len(split) != 2:
189 raise option_error
190
191 check_name, option_value = split
192 if not option_value:
193 raise option_error
194
195 split = option_value.split(b"=")
196 if len(split) != 2:
197 raise option_error
198
199 option_name, value = split
200 if not value:
201 raise option_error
202
203 path = b"%s:%s" % (check_name, option_name)
204
205 matching_checks = find_checks(
206 check_name,
207 table=table,
208 alias_table=alias_table,
209 full_pyramid=full_pyramid,
210 )
211 for name in matching_checks:
212 check = checks.get(name)
213 if check is None:
214 msg = _(b"specified option '%s' for unselected check '%s'\n")
215 raise error.InputError(msg % (name, option_name))
216
217 assert hasattr(check, "func") # help Pytype
218
219 if not hasattr(check.func, "options"):
220 raise error.InputError(
221 _(b"check '%s' has no option '%s'") % (name, option_name)
222 )
223
224 try:
225 matching_option = next(
226 (o for o in check.func.options if o[0] == option_name)
227 )
228 except StopIteration:
229 raise error.InputError(
230 _(b"check '%s' has no option '%s'") % (name, option_name)
231 )
232
233 # transform the argument from cli string to the expected Python type
234 _name, typ, _docstring = matching_option
235
236 as_typed = None
237 if isinstance(typ, bool):
238 as_bool = stringutil.parsebool(value)
239 if as_bool is None:
240 raise error.InputError(
241 _(b"'%s' is not a boolean ('%s')") % (path, value)
242 )
243 as_typed = as_bool
244 elif isinstance(typ, list):
245 as_list = stringutil.parselist(value)
246 if as_list is None:
247 raise error.InputError(
248 _(b"'%s' is not a list ('%s')") % (path, value)
249 )
250 as_typed = as_list
251 else:
252 raise error.ProgrammingError(b"unsupported type %s", type(typ))
253
254 if option_name in to_modify[name]:
255 raise error.InputError(
256 _(b"duplicated option '%s' for '%s'") % (option_name, name)
257 )
258 else:
259 assert as_typed is not None
260 to_modify[name][option_name] = as_typed
261
262 # Manage case where a check is set but without command line options
263 # it will later be set with default check options values
264 for name, f in checks.items():
265 if name not in to_modify:
266 to_modify[name] = {}
267
268 # Merge default options with command line options
269 for check_name, cmd_options in to_modify.items():
270 check = checks.get(check_name)
271 func = checks[check_name]
272 merged_options = {}
273 # help Pytype
274 assert check is not None
275 assert check.func is not None
276 assert hasattr(check.func, "options")
277
278 if check.func.options:
279 # copy the default value in case it's mutable (list, etc.)
280 merged_options = {
281 o[0]: copy.deepcopy(o[1]) for o in check.func.options
282 }
283 if cmd_options:
284 for k, v in cmd_options.items():
285 merged_options[k] = v
286 options = pycompat.strkwargs(merged_options)
287 checks[check_name] = functools.partial(func, **options)
288 ui.debug(b"merged options for '%s': '%r'\n" % (check_name, options))
289
290 return checks
291
292
293 def get_checks(
294 repo,
295 ui,
296 names=None,
297 options=None,
298 table=None,
299 alias_table=None,
300 full_pyramid=None,
301 ):
302 """Given a list of function names and optionally a list of
303 options, return matched checks with merged options (command line options
304 values take precedence on default ones)
305
306 It runs find checks, then resolve options and returns a dict of matched
307 functions with resolved options.
308 """
309 funcs = {}
310
311 if names is None:
312 names = []
313
314 if options is None:
315 options = []
316
317 # find checks
318 for name in names:
319 matched = find_checks(
320 name,
321 table=table,
322 alias_table=alias_table,
323 full_pyramid=full_pyramid,
324 )
325 matched_names = b", ".join(matched)
326 ui.debug(b"found checks '%s' for name '%s'\n" % (matched_names, name))
327 funcs.update(matched)
328
329 funcs = {n: functools.partial(f, ui, repo) for n, f in funcs.items()}
330
331 # resolve options
332 checks = pass_options(
333 ui,
334 funcs,
335 options,
336 table=table,
337 alias_table=alias_table,
338 full_pyramid=full_pyramid,
339 )
340
341 return checks
@@ -0,0 +1,399 b''
1 # Test admin commands
2
3 import functools
4 import unittest
5 from mercurial.i18n import _
6 from mercurial import error, ui as uimod
7 from mercurial import registrar
8 from mercurial.admin import verify
9
10
11 class TestAdminVerifyFindChecks(unittest.TestCase):
12 def __init__(self, *args, **kwargs):
13 super().__init__(*args, **kwargs)
14 self.ui = uimod.ui.load()
15 self.repo = b"fake-repo"
16
17 def cleanup_table(self):
18 self.table = {}
19 self.alias_table = {}
20 self.pyramid = {}
21
22 self.addCleanup(cleanup_table, self)
23
24 def setUp(self):
25 self.table = {}
26 self.alias_table = {}
27 self.pyramid = {}
28 check = registrar.verify_check(self.table, self.alias_table)
29
30 # mock some fake check method for tests purpose
31 @check(
32 b"test.dummy",
33 alias=b"dummy",
34 options=[],
35 )
36 def check_dummy(ui, repo, **options):
37 return options
38
39 @check(
40 b"test.fake",
41 alias=b"fake",
42 options=[
43 (b'a', False, _(b'a boolean value (default: False)')),
44 (b'b', True, _(b'a boolean value (default: True)')),
45 (b'c', [], _(b'a list')),
46 ],
47 )
48 def check_fake(ui, repo, **options):
49 return options
50
51 # alias in the middle of a hierarchy
52 check(
53 b"test.noop",
54 alias=b"noop",
55 options=[],
56 )(verify.noop_func)
57
58 @check(
59 b"test.noop.deeper",
60 alias=b"deeper",
61 options=[
62 (b'y', True, _(b'a boolean value (default: True)')),
63 (b'z', [], _(b'a list')),
64 ],
65 )
66 def check_noop_deeper(ui, repo, **options):
67 return options
68
69 # args wrapper utilities
70 def find_checks(self, name):
71 return verify.find_checks(
72 name=name,
73 table=self.table,
74 alias_table=self.alias_table,
75 full_pyramid=self.pyramid,
76 )
77
78 def pass_options(self, checks, options):
79 return verify.pass_options(
80 self.ui,
81 checks,
82 options,
83 table=self.table,
84 alias_table=self.alias_table,
85 full_pyramid=self.pyramid,
86 )
87
88 def get_checks(self, names, options):
89 return verify.get_checks(
90 self.repo,
91 self.ui,
92 names=names,
93 options=options,
94 table=self.table,
95 alias_table=self.alias_table,
96 full_pyramid=self.pyramid,
97 )
98
99 # tests find_checks
100 def test_find_checks_empty_name(self):
101 with self.assertRaises(error.InputError):
102 self.find_checks(name=b"")
103
104 def test_find_checks_wrong_name(self):
105 with self.assertRaises(error.InputError):
106 self.find_checks(name=b"unknown")
107
108 def test_find_checks_dummy(self):
109 name = b"test.dummy"
110 found = self.find_checks(name=name)
111 self.assertEqual(len(found), 1)
112 self.assertIn(name, found)
113 meth = found[name]
114 self.assertTrue(callable(meth))
115 self.assertEqual(len(meth.options), 0)
116
117 def test_find_checks_fake(self):
118 name = b"test.fake"
119 found = self.find_checks(name=name)
120 self.assertEqual(len(found), 1)
121 self.assertIn(name, found)
122 meth = found[name]
123 self.assertTrue(callable(meth))
124 self.assertEqual(len(meth.options), 3)
125
126 def test_find_checks_noop(self):
127 name = b"test.noop.deeper"
128 found = self.find_checks(name=name)
129 self.assertEqual(len(found), 1)
130 self.assertIn(name, found)
131 meth = found[name]
132 self.assertTrue(callable(meth))
133 self.assertEqual(len(meth.options), 2)
134
135 def test_find_checks_from_aliases(self):
136 found = self.find_checks(name=b"dummy")
137 self.assertEqual(len(found), 1)
138 self.assertIn(b"test.dummy", found)
139
140 found = self.find_checks(name=b"fake")
141 self.assertEqual(len(found), 1)
142 self.assertIn(b"test.fake", found)
143
144 found = self.find_checks(name=b"deeper")
145 self.assertEqual(len(found), 1)
146 self.assertIn(b"test.noop.deeper", found)
147
148 def test_find_checks_from_root(self):
149 found = self.find_checks(name=b"test")
150 self.assertEqual(len(found), 3)
151 self.assertIn(b"test.dummy", found)
152 self.assertIn(b"test.fake", found)
153 self.assertIn(b"test.noop.deeper", found)
154
155 def test_find_checks_from_intermediate(self):
156 found = self.find_checks(name=b"test.noop")
157 self.assertEqual(len(found), 1)
158 self.assertIn(b"test.noop.deeper", found)
159
160 def test_find_checks_from_parent_dot_name(self):
161 found = self.find_checks(name=b"noop.deeper")
162 self.assertEqual(len(found), 1)
163 self.assertIn(b"test.noop.deeper", found)
164
165 # tests pass_options
166 def test_pass_options_no_checks_no_options(self):
167 checks = {}
168 options = []
169
170 with self.assertRaises(error.Error):
171 self.pass_options(checks=checks, options=options)
172
173 def test_pass_options_fake_empty_options(self):
174 checks = self.find_checks(name=b"test.fake")
175 funcs = {
176 n: functools.partial(f, self.ui, self.repo)
177 for n, f in checks.items()
178 }
179 options = []
180 # should end with default options
181 expected_options = {"a": False, "b": True, "c": []}
182 func = self.pass_options(checks=funcs, options=options)
183
184 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
185
186 def test_pass_options_fake_non_existing_options(self):
187 checks = self.find_checks(name=b"test.fake")
188 funcs = {
189 n: functools.partial(f, self.ui, self.repo)
190 for n, f in checks.items()
191 }
192
193 with self.assertRaises(error.InputError):
194 options = [b"test.fake:boom=yes"]
195 self.pass_options(checks=funcs, options=options)
196
197 def test_pass_options_fake_unrelated_options(self):
198 checks = self.find_checks(name=b"test.fake")
199 funcs = {
200 n: functools.partial(f, self.ui, self.repo)
201 for n, f in checks.items()
202 }
203 options = [b"test.noop.deeper:y=yes"]
204
205 with self.assertRaises(error.InputError):
206 self.pass_options(checks=funcs, options=options)
207
208 def test_pass_options_fake_set_option(self):
209 checks = self.find_checks(name=b"test.fake")
210 funcs = {
211 n: functools.partial(f, self.ui, self.repo)
212 for n, f in checks.items()
213 }
214 options = [b"test.fake:a=yes"]
215 expected_options = {"a": True, "b": True, "c": []}
216 func = self.pass_options(checks=funcs, options=options)
217
218 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
219
220 def test_pass_options_fake_set_option_with_alias(self):
221 checks = self.find_checks(name=b"test.fake")
222 funcs = {
223 n: functools.partial(f, self.ui, self.repo)
224 for n, f in checks.items()
225 }
226 options = [b"fake:a=yes"]
227 expected_options = {"a": True, "b": True, "c": []}
228 func = self.pass_options(checks=funcs, options=options)
229
230 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
231
232 def test_pass_options_fake_set_all_option(self):
233 checks = self.find_checks(name=b"test.fake")
234 funcs = {
235 n: functools.partial(f, self.ui, self.repo)
236 for n, f in checks.items()
237 }
238 options = [b"test.fake:a=yes", b"test.fake:b=no", b"test.fake:c=0,1,2"]
239 expected_options = {"a": True, "b": False, "c": [b"0", b"1", b"2"]}
240 func = self.pass_options(checks=funcs, options=options)
241
242 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
243
244 def test_pass_options_fake_set_all_option_plus_unexisting(self):
245 checks = self.find_checks(name=b"test.fake")
246 funcs = {
247 n: functools.partial(f, self.ui, self.repo)
248 for n, f in checks.items()
249 }
250 options = [
251 b"test.fake:a=yes",
252 b"test.fake:b=no",
253 b"test.fake:c=0,1,2",
254 b"test.fake:d=0",
255 ]
256
257 with self.assertRaises(error.InputError):
258 self.pass_options(checks=funcs, options=options)
259
260 def test_pass_options_fake_duplicate_option(self):
261 checks = self.find_checks(name=b"test.fake")
262 funcs = {
263 n: functools.partial(f, self.ui, self.repo)
264 for n, f in checks.items()
265 }
266 options = [
267 b"test.fake:a=yes",
268 b"test.fake:a=no",
269 ]
270
271 with self.assertRaises(error.InputError):
272 self.pass_options(checks=funcs, options=options)
273
274 def test_pass_options_fake_set_malformed_option(self):
275 checks = self.find_checks(name=b"test.fake")
276 funcs = {
277 n: functools.partial(f, self.ui, self.repo)
278 for n, f in checks.items()
279 }
280 options = [
281 b"test.fake:ayes",
282 b"test.fake:b==no",
283 b"test.fake=",
284 b"test.fake:",
285 b"test.fa=ke:d=0",
286 b"test.fa=ke:d=0",
287 ]
288
289 for opt in options:
290 with self.assertRaises(error.InputError):
291 self.pass_options(checks=funcs, options=[opt])
292
293 def test_pass_options_types(self):
294 checks = self.find_checks(name=b"test.fake")
295 funcs = {
296 n: functools.partial(f, self.ui, self.repo)
297 for n, f in checks.items()
298 }
299 # boolean, yes/no
300 options = [b"test.fake:a=yes", b"test.fake:b=no"]
301 expected_options = {"a": True, "b": False, "c": []}
302 func = self.pass_options(checks=funcs, options=options)
303
304 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
305
306 # boolean, 0/1
307 options = [b"test.fake:a=1", b"test.fake:b=0"]
308 expected_options = {"a": True, "b": False, "c": []}
309 func = self.pass_options(checks=funcs, options=options)
310
311 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
312
313 # boolean, true/false
314 options = [b"test.fake:a=true", b"test.fake:b=false"]
315 expected_options = {"a": True, "b": False, "c": []}
316 func = self.pass_options(checks=funcs, options=options)
317
318 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
319
320 # boolean, wrong type
321 options = [b"test.fake:a=si"]
322 with self.assertRaises(error.InputError):
323 self.pass_options(checks=funcs, options=options)
324
325 # lists
326 options = [b"test.fake:c=0,1,2"]
327 expected_options = {"a": False, "b": True, "c": [b"0", b"1", b"2"]}
328 func = self.pass_options(checks=funcs, options=options)
329
330 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
331
332 options = [b"test.fake:c=x,y,z"]
333 expected_options = {"a": False, "b": True, "c": [b"x", b"y", b"z"]}
334 func = self.pass_options(checks=funcs, options=options)
335
336 self.assertDictEqual(func[b"test.fake"].keywords, expected_options)
337
338 # tests get_checks
339 def test_get_checks_fake(self):
340 funcs = self.get_checks(
341 names=[b"test.fake"], options=[b"test.fake:a=yes"]
342 )
343 options = funcs.get(b"test.fake").keywords
344 expected_options = {"a": True, "b": True, "c": []}
345 self.assertDictEqual(options, expected_options)
346
347 def test_get_checks_multiple_mixed_with_defaults(self):
348 funcs = self.get_checks(
349 names=[b"test.fake", b"test.noop.deeper", b"test.dummy"],
350 options=[
351 b"test.noop.deeper:y=no",
352 b"test.noop.deeper:z=-1,0,1",
353 ],
354 )
355 options = funcs.get(b"test.fake").keywords
356 expected_options = {"a": False, "b": True, "c": []}
357 self.assertDictEqual(options, expected_options)
358
359 options = funcs.get(b"test.noop.deeper").keywords
360 expected_options = {"y": False, "z": [b"-1", b"0", b"1"]}
361 self.assertDictEqual(options, expected_options)
362
363 options = funcs.get(b"test.dummy").keywords
364 expected_options = {}
365 self.assertDictEqual(options, expected_options)
366
367 def test_broken_pyramid(self):
368 """Check that we detect pyramids that can't resolve"""
369 table = {}
370 alias_table = {}
371 pyramid = {}
372 check = registrar.verify_check(table, alias_table)
373
374 # Create two checks that clash
375 @check(b"test.wrong.intermediate")
376 def check_dummy(ui, repo, **options):
377 return options
378
379 @check(b"test.wrong.intermediate.thing")
380 def check_fake(ui, repo, **options):
381 return options
382
383 with self.assertRaises(error.ProgrammingError) as e:
384 verify.get_checks(
385 self.repo,
386 self.ui,
387 names=[b"test.wrong.intermediate"],
388 options=[],
389 table=table,
390 alias_table=alias_table,
391 full_pyramid=pyramid,
392 )
393 assert "`verify.noop_func`" in str(e.exception), str(e.exception)
394
395
396 if __name__ == '__main__':
397 import silenttestrunner
398
399 silenttestrunner.main(__name__)
@@ -0,0 +1,49 b''
1 Test admin::verify
2
3 $ hg init admin-verify
4 $ cd admin-verify
5
6 Test normal output
7
8 $ hg admin::verify -c dirstate
9 running 1 checks
10 running working-copy.dirstate
11 checking dirstate
12
13 Quiet works
14
15 $ hg admin::verify -c dirstate --quiet
16
17 Test no check no options
18
19 $ hg admin::verify
20 abort: `checks` required
21 [255]
22
23 Test single check without options
24
25 $ hg admin::verify -c working-copy.dirstate
26 running 1 checks
27 running working-copy.dirstate
28 checking dirstate
29
30 Test single check (alias) without options
31
32 $ hg admin::verify -c dirstate
33 running 1 checks
34 running working-copy.dirstate
35 checking dirstate
36
37 Test wrong check name without options
38
39 $ hg admin::verify -c working-copy.dir
40 abort: unknown check working-copy.dir
41 (did you mean working-copy.dirstate?)
42 [10]
43
44 Test wrong alias without options
45
46 $ hg admin::verify -c dir
47 abort: unknown check dir
48 [10]
49
@@ -5,7 +5,45 b''
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 . import registrar
8 from .i18n import _
9 from .admin import verify
10 from . import error, registrar, transaction
11
9
12
10 table = {}
13 table = {}
11 command = registrar.command(table)
14 command = registrar.command(table)
15
16
17 @command(
18 b'admin::verify',
19 [
20 (b'c', b'check', [], _(b'add a check'), _(b'CHECK')),
21 (b'o', b'option', [], _(b'pass an option to a check'), _(b'OPTION')),
22 ],
23 helpcategory=command.CATEGORY_MAINTENANCE,
24 )
25 def admin_verify(ui, repo, **opts):
26 """verify the integrity of the repository
27
28 Alternative UI to `hg verify` with a lot more control over the
29 verification process and better error reporting.
30 """
31
32 if not repo.url().startswith(b'file:'):
33 raise error.Abort(_(b"cannot verify bundle or remote repos"))
34
35 if transaction.has_abandoned_transaction(repo):
36 ui.warn(_(b"abandoned transaction found - run hg recover\n"))
37
38 checks = opts.get("check", [])
39 options = opts.get("option", [])
40
41 funcs = verify.get_checks(repo, ui, names=checks, options=options)
42
43 ui.status(_(b"running %d checks\n") % len(funcs))
44 # Done in two times so the execution is separated from the resolving step
45 for name, func in sorted(funcs.items(), key=lambda x: x[0]):
46 ui.status(_(b"running %s\n") % name)
47 errors = func()
48 if errors:
49 ui.warn(_(b"found %d errors\n") % len(errors))
@@ -7961,6 +7961,9 b' def verify(ui, repo, **opts):'
7961 for more information about recovery from corruption of the
7961 for more information about recovery from corruption of the
7962 repository.
7962 repository.
7963
7963
7964 For an alternative UI with a lot more control over the verification
7965 process and better error reporting, try `hg help admin::verify`.
7966
7964 Returns 0 on success, 1 if errors are encountered.
7967 Returns 0 on success, 1 if errors are encountered.
7965 """
7968 """
7966 level = None
7969 level = None
@@ -6,6 +6,7 b''
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
8
9 from typing import Any, List, Optional, Tuple
9 from . import (
10 from . import (
10 configitems,
11 configitems,
11 error,
12 error,
@@ -533,3 +534,30 b' class internalmerge(_funcregistrarbase):'
533
534
534 # actual capabilities, which this internal merge tool has
535 # actual capabilities, which this internal merge tool has
535 func.capabilities = {b"binary": binarycap, b"symlink": symlinkcap}
536 func.capabilities = {b"binary": binarycap, b"symlink": symlinkcap}
537
538
539 class verify_check(_funcregistrarbase):
540 """Decorator to register a check for admin::verify
541
542 options is a list of (name, default value, help) to be passed to the check
543 """
544
545 def __init__(self, table=None, alias_table=None):
546 super().__init__(table)
547 if alias_table is None:
548 self._alias_table = {}
549 else:
550 self._alias_table = alias_table
551
552 def _extrasetup(
553 self,
554 name,
555 func,
556 alias: Optional[bytes] = None,
557 options: Optional[List[Tuple[bytes, Any, bytes]]] = None,
558 ):
559 func.alias = alias
560 func.options = options
561
562 if alias:
563 self._alias_table[alias] = name
@@ -3,6 +3,7 b' Show all commands except debug commands'
3 abort
3 abort
4 add
4 add
5 addremove
5 addremove
6 admin::verify
6 annotate
7 annotate
7 archive
8 archive
8 backout
9 backout
@@ -65,6 +66,7 b' Show all commands that start with "a"'
65 abort
66 abort
66 add
67 add
67 addremove
68 addremove
69 admin::verify
68 annotate
70 annotate
69 archive
71 archive
70
72
@@ -257,6 +259,7 b' Show all commands + options'
257 abort: dry-run
259 abort: dry-run
258 add: include, exclude, subrepos, dry-run
260 add: include, exclude, subrepos, dry-run
259 addremove: similarity, subrepos, include, exclude, dry-run
261 addremove: similarity, subrepos, include, exclude, dry-run
262 admin::verify: check, option
260 annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, skip, ignore-all-space, ignore-space-change, ignore-blank-lines, ignore-space-at-eol, include, exclude, template
263 annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, skip, ignore-all-space, ignore-space-change, ignore-blank-lines, ignore-space-at-eol, include, exclude, template
261 archive: no-decode, prefix, rev, type, subrepos, include, exclude
264 archive: no-decode, prefix, rev, type, subrepos, include, exclude
262 backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
265 backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
@@ -378,6 +378,8 b' Testing -h/--help:'
378
378
379 Repository maintenance:
379 Repository maintenance:
380
380
381 admin::verify
382 verify the integrity of the repository
381 manifest output the current or given revision of the project manifest
383 manifest output the current or given revision of the project manifest
382 recover roll back an interrupted transaction
384 recover roll back an interrupted transaction
383 verify verify the integrity of the repository
385 verify verify the integrity of the repository
@@ -513,6 +515,8 b' Testing -h/--help:'
513
515
514 Repository maintenance:
516 Repository maintenance:
515
517
518 admin::verify
519 verify the integrity of the repository
516 manifest output the current or given revision of the project manifest
520 manifest output the current or given revision of the project manifest
517 recover roll back an interrupted transaction
521 recover roll back an interrupted transaction
518 verify verify the integrity of the repository
522 verify verify the integrity of the repository
@@ -77,6 +77,8 b' Test hiding some commands (which also ha'
77
77
78 Repository maintenance:
78 Repository maintenance:
79
79
80 admin::verify
81 verify the integrity of the repository
80 manifest output the current or given revision of the project manifest
82 manifest output the current or given revision of the project manifest
81 recover roll back an interrupted transaction
83 recover roll back an interrupted transaction
82 verify verify the integrity of the repository
84 verify verify the integrity of the repository
@@ -216,6 +218,8 b' Test hiding some topics.'
216
218
217 Repository maintenance:
219 Repository maintenance:
218
220
221 admin::verify
222 verify the integrity of the repository
219 manifest output the current or given revision of the project manifest
223 manifest output the current or given revision of the project manifest
220 recover roll back an interrupted transaction
224 recover roll back an interrupted transaction
221 verify verify the integrity of the repository
225 verify verify the integrity of the repository
@@ -129,6 +129,8 b' the extension is unknown.'
129
129
130 Repository maintenance:
130 Repository maintenance:
131
131
132 admin::verify
133 verify the integrity of the repository
132 manifest output the current or given revision of the project manifest
134 manifest output the current or given revision of the project manifest
133 recover roll back an interrupted transaction
135 recover roll back an interrupted transaction
134 verify verify the integrity of the repository
136 verify verify the integrity of the repository
@@ -260,6 +262,8 b' the extension is unknown.'
260
262
261 Repository maintenance:
263 Repository maintenance:
262
264
265 admin::verify
266 verify the integrity of the repository
263 manifest output the current or given revision of the project manifest
267 manifest output the current or given revision of the project manifest
264 recover roll back an interrupted transaction
268 recover roll back an interrupted transaction
265 verify verify the integrity of the repository
269 verify verify the integrity of the repository
@@ -604,9 +608,16 b' Test ambiguous command help'
604 $ hg help ad
608 $ hg help ad
605 list of commands:
609 list of commands:
606
610
611 Working directory management:
612
607 add add the specified files on the next commit
613 add add the specified files on the next commit
608 addremove add all new files, delete all missing files
614 addremove add all new files, delete all missing files
609
615
616 Repository maintenance:
617
618 admin::verify
619 verify the integrity of the repository
620
610 (use 'hg help -v ad' to show built-in aliases and global options)
621 (use 'hg help -v ad' to show built-in aliases and global options)
611
622
612 Test command without options
623 Test command without options
@@ -626,6 +637,9 b' Test command without options'
626 Please see https://mercurial-scm.org/wiki/RepositoryCorruption for more
637 Please see https://mercurial-scm.org/wiki/RepositoryCorruption for more
627 information about recovery from corruption of the repository.
638 information about recovery from corruption of the repository.
628
639
640 For an alternative UI with a lot more control over the verification
641 process and better error reporting, try 'hg help admin::verify'.
642
629 Returns 0 on success, 1 if errors are encountered.
643 Returns 0 on success, 1 if errors are encountered.
630
644
631 options:
645 options:
@@ -2650,6 +2664,13 b' Dish up an empty repo; serve it cold.'
2650 add all new files, delete all missing files
2664 add all new files, delete all missing files
2651 </td></tr>
2665 </td></tr>
2652 <tr><td>
2666 <tr><td>
2667 <a href="/help/admin::verify">
2668 admin::verify
2669 </a>
2670 </td><td>
2671 verify the integrity of the repository
2672 </td></tr>
2673 <tr><td>
2653 <a href="/help/archive">
2674 <a href="/help/archive">
2654 archive
2675 archive
2655 </a>
2676 </a>
@@ -2112,6 +2112,10 b' help/ shows help topics'
2112 "topic": "addremove"
2112 "topic": "addremove"
2113 },
2113 },
2114 {
2114 {
2115 "summary": "verify the integrity of the repository",
2116 "topic": "admin::verify"
2117 },
2118 {
2115 "summary": "create an unversioned archive of a repository revision",
2119 "summary": "create an unversioned archive of a repository revision",
2116 "topic": "archive"
2120 "topic": "archive"
2117 },
2121 },
General Comments 0
You need to be logged in to leave comments. Login now