##// END OF EJS Templates
admin-verify: pass p1 down to the dirstate function...
Raphaël Gomès -
r52506:dcb00d5c stable
parent child Browse files
Show More
@@ -1,341 +1,340
1 # admin/verify.py - better repository integrity checking for Mercurial
1 # admin/verify.py - better repository integrity checking for Mercurial
2 #
2 #
3 # Copyright 2023 Octobus <contact@octobus.net>
3 # Copyright 2023 Octobus <contact@octobus.net>
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 import collections
8 import collections
9 import copy
9 import copy
10 import functools
10 import functools
11
11
12 from ..i18n import _
12 from ..i18n import _
13 from .. import error, pycompat, registrar, requirements
13 from .. import error, pycompat, registrar, requirements
14 from ..utils import stringutil
14 from ..utils import stringutil
15
15
16
16
17 verify_table = {}
17 verify_table = {}
18 verify_alias_table = {}
18 verify_alias_table = {}
19 check = registrar.verify_check(verify_table, verify_alias_table)
19 check = registrar.verify_check(verify_table, verify_alias_table)
20
20
21
21
22 # Use this to declare options/aliases in the middle of the hierarchy.
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.
23 # Checks like these are not run themselves and cannot have a body.
24 # For an example, see the `revlogs` check.
24 # For an example, see the `revlogs` check.
25 def noop_func(*args, **kwargs):
25 def noop_func(*args, **kwargs):
26 return
26 return
27
27
28
28
29 @check(b"working-copy.dirstate", alias=b"dirstate")
29 @check(b"working-copy.dirstate", alias=b"dirstate")
30 def check_dirstate(ui, repo, **options):
30 def check_dirstate(ui, repo, **options):
31 ui.status(_(b"checking dirstate\n"))
31 ui.status(_(b"checking dirstate\n"))
32
32
33 parent1, parent2 = repo.dirstate.parents()
33 parent1, parent2 = repo.dirstate.parents()
34 m1 = repo[parent1].manifest()
34 m1 = repo[parent1].manifest()
35 m2 = repo[parent2].manifest()
35 m2 = repo[parent2].manifest()
36 errors = 0
36 errors = 0
37
37
38 is_narrow = requirements.NARROW_REQUIREMENT in repo.requirements
38 is_narrow = requirements.NARROW_REQUIREMENT in repo.requirements
39 narrow_matcher = repo.narrowmatch() if is_narrow else None
39 narrow_matcher = repo.narrowmatch() if is_narrow else None
40
40 for err in repo.dirstate.verify(m1, m2, parent1, narrow_matcher):
41 for err in repo.dirstate.verify(m1, m2, narrow_matcher):
42 ui.warn(err[0] % err[1:])
41 ui.warn(err[0] % err[1:])
43 errors += 1
42 errors += 1
44
43
45 return errors
44 return errors
46
45
47
46
48 # Tree of all checks and their associated function
47 # Tree of all checks and their associated function
49 pyramid = {}
48 pyramid = {}
50
49
51
50
52 def build_pyramid(table, full_pyramid):
51 def build_pyramid(table, full_pyramid):
53 """Create a pyramid of checks of the registered checks.
52 """Create a pyramid of checks of the registered checks.
54 It is a name-based hierarchy that can be arbitrarily nested."""
53 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):
54 for entry, func in sorted(table.items(), key=lambda x: x[0], reverse=True):
56 cursor = full_pyramid
55 cursor = full_pyramid
57 levels = entry.split(b".")
56 levels = entry.split(b".")
58 for level in levels[:-1]:
57 for level in levels[:-1]:
59 current_node = cursor.setdefault(level, {})
58 current_node = cursor.setdefault(level, {})
60 cursor = current_node
59 cursor = current_node
61 if cursor.get(levels[-1]) is None:
60 if cursor.get(levels[-1]) is None:
62 cursor[levels[-1]] = (entry, func)
61 cursor[levels[-1]] = (entry, func)
63 elif func is not noop_func:
62 elif func is not noop_func:
64 m = b"intermediate checks need to use `verify.noop_func`"
63 m = b"intermediate checks need to use `verify.noop_func`"
65 raise error.ProgrammingError(m)
64 raise error.ProgrammingError(m)
66
65
67
66
68 def find_checks(name, table=None, alias_table=None, full_pyramid=None):
67 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
68 """Find all checks for a given name and returns a dict of
70 (qualified_check_name, check_function)
69 (qualified_check_name, check_function)
71
70
72 # Examples
71 # Examples
73
72
74 Using a full qualified name:
73 Using a full qualified name:
75 "working-copy.dirstate" -> {
74 "working-copy.dirstate" -> {
76 "working-copy.dirstate": CF,
75 "working-copy.dirstate": CF,
77 }
76 }
78
77
79 Using a *prefix* of a qualified name:
78 Using a *prefix* of a qualified name:
80 "store.revlogs" -> {
79 "store.revlogs" -> {
81 "store.revlogs.changelog": CF,
80 "store.revlogs.changelog": CF,
82 "store.revlogs.manifestlog": CF,
81 "store.revlogs.manifestlog": CF,
83 "store.revlogs.filelog": CF,
82 "store.revlogs.filelog": CF,
84 }
83 }
85
84
86 Using a defined alias:
85 Using a defined alias:
87 "revlogs" -> {
86 "revlogs" -> {
88 "store.revlogs.changelog": CF,
87 "store.revlogs.changelog": CF,
89 "store.revlogs.manifestlog": CF,
88 "store.revlogs.manifestlog": CF,
90 "store.revlogs.filelog": CF,
89 "store.revlogs.filelog": CF,
91 }
90 }
92
91
93 Using something that is none of the above will be an error.
92 Using something that is none of the above will be an error.
94 """
93 """
95 if table is None:
94 if table is None:
96 table = verify_table
95 table = verify_table
97 if alias_table is None:
96 if alias_table is None:
98 alias_table = verify_alias_table
97 alias_table = verify_alias_table
99
98
100 if name == b"full":
99 if name == b"full":
101 return table
100 return table
102 checks = {}
101 checks = {}
103
102
104 # is it a full name?
103 # is it a full name?
105 check = table.get(name)
104 check = table.get(name)
106
105
107 if check is None:
106 if check is None:
108 # is it an alias?
107 # is it an alias?
109 qualified_name = alias_table.get(name)
108 qualified_name = alias_table.get(name)
110 if qualified_name is not None:
109 if qualified_name is not None:
111 name = qualified_name
110 name = qualified_name
112 check = table.get(name)
111 check = table.get(name)
113 else:
112 else:
114 split = name.split(b".", 1)
113 split = name.split(b".", 1)
115 if len(split) == 2:
114 if len(split) == 2:
116 # split[0] can be an alias
115 # split[0] can be an alias
117 qualified_name = alias_table.get(split[0])
116 qualified_name = alias_table.get(split[0])
118 if qualified_name is not None:
117 if qualified_name is not None:
119 name = b"%s.%s" % (qualified_name, split[1])
118 name = b"%s.%s" % (qualified_name, split[1])
120 check = table.get(name)
119 check = table.get(name)
121 else:
120 else:
122 qualified_name = name
121 qualified_name = name
123
122
124 # Maybe it's a subtree in the check hierarchy that does not
123 # Maybe it's a subtree in the check hierarchy that does not
125 # have an explicit alias.
124 # have an explicit alias.
126 levels = name.split(b".")
125 levels = name.split(b".")
127 if full_pyramid is not None:
126 if full_pyramid is not None:
128 if not full_pyramid:
127 if not full_pyramid:
129 build_pyramid(table, full_pyramid)
128 build_pyramid(table, full_pyramid)
130
129
131 pyramid.clear()
130 pyramid.clear()
132 pyramid.update(full_pyramid.items())
131 pyramid.update(full_pyramid.items())
133 else:
132 else:
134 build_pyramid(table, pyramid)
133 build_pyramid(table, pyramid)
135
134
136 subtree = pyramid
135 subtree = pyramid
137 # Find subtree
136 # Find subtree
138 for level in levels:
137 for level in levels:
139 subtree = subtree.get(level)
138 subtree = subtree.get(level)
140 if subtree is None:
139 if subtree is None:
141 hint = error.getsimilar(list(alias_table) + list(table), name)
140 hint = error.getsimilar(list(alias_table) + list(table), name)
142 hint = error.similarity_hint(hint)
141 hint = error.similarity_hint(hint)
143
142
144 raise error.InputError(_(b"unknown check %s" % name), hint=hint)
143 raise error.InputError(_(b"unknown check %s" % name), hint=hint)
145
144
146 # Get all checks in that subtree
145 # Get all checks in that subtree
147 if isinstance(subtree, dict):
146 if isinstance(subtree, dict):
148 stack = list(subtree.items())
147 stack = list(subtree.items())
149 while stack:
148 while stack:
150 current_name, entry = stack.pop()
149 current_name, entry = stack.pop()
151 if isinstance(entry, dict):
150 if isinstance(entry, dict):
152 stack.extend(entry.items())
151 stack.extend(entry.items())
153 else:
152 else:
154 # (qualified_name, func)
153 # (qualified_name, func)
155 checks[entry[0]] = entry[1]
154 checks[entry[0]] = entry[1]
156 else:
155 else:
157 checks[name] = check
156 checks[name] = check
158
157
159 return checks
158 return checks
160
159
161
160
162 def pass_options(
161 def pass_options(
163 ui,
162 ui,
164 checks,
163 checks,
165 options,
164 options,
166 table=None,
165 table=None,
167 alias_table=None,
166 alias_table=None,
168 full_pyramid=None,
167 full_pyramid=None,
169 ):
168 ):
170 """Given a dict of checks (fully qualified name to function), and a list
169 """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
170 of options as given by the user, pass each option down to the right check
172 function."""
171 function."""
173 ui.debug(b"passing options to check functions\n")
172 ui.debug(b"passing options to check functions\n")
174 to_modify = collections.defaultdict(dict)
173 to_modify = collections.defaultdict(dict)
175
174
176 if not checks:
175 if not checks:
177 raise error.Error(_(b"`checks` required"))
176 raise error.Error(_(b"`checks` required"))
178
177
179 for option in sorted(options):
178 for option in sorted(options):
180 split = option.split(b":")
179 split = option.split(b":")
181 hint = _(
180 hint = _(
182 b"syntax is 'check:option=value', "
181 b"syntax is 'check:option=value', "
183 b"eg. revlogs.changelog:copies=yes"
182 b"eg. revlogs.changelog:copies=yes"
184 )
183 )
185 option_error = error.InputError(
184 option_error = error.InputError(
186 _(b"invalid option '%s'") % option, hint=hint
185 _(b"invalid option '%s'") % option, hint=hint
187 )
186 )
188 if len(split) != 2:
187 if len(split) != 2:
189 raise option_error
188 raise option_error
190
189
191 check_name, option_value = split
190 check_name, option_value = split
192 if not option_value:
191 if not option_value:
193 raise option_error
192 raise option_error
194
193
195 split = option_value.split(b"=")
194 split = option_value.split(b"=")
196 if len(split) != 2:
195 if len(split) != 2:
197 raise option_error
196 raise option_error
198
197
199 option_name, value = split
198 option_name, value = split
200 if not value:
199 if not value:
201 raise option_error
200 raise option_error
202
201
203 path = b"%s:%s" % (check_name, option_name)
202 path = b"%s:%s" % (check_name, option_name)
204
203
205 matching_checks = find_checks(
204 matching_checks = find_checks(
206 check_name,
205 check_name,
207 table=table,
206 table=table,
208 alias_table=alias_table,
207 alias_table=alias_table,
209 full_pyramid=full_pyramid,
208 full_pyramid=full_pyramid,
210 )
209 )
211 for name in matching_checks:
210 for name in matching_checks:
212 check = checks.get(name)
211 check = checks.get(name)
213 if check is None:
212 if check is None:
214 msg = _(b"specified option '%s' for unselected check '%s'\n")
213 msg = _(b"specified option '%s' for unselected check '%s'\n")
215 raise error.InputError(msg % (name, option_name))
214 raise error.InputError(msg % (name, option_name))
216
215
217 assert hasattr(check, "func") # help Pytype
216 assert hasattr(check, "func") # help Pytype
218
217
219 if not hasattr(check.func, "options"):
218 if not hasattr(check.func, "options"):
220 raise error.InputError(
219 raise error.InputError(
221 _(b"check '%s' has no option '%s'") % (name, option_name)
220 _(b"check '%s' has no option '%s'") % (name, option_name)
222 )
221 )
223
222
224 try:
223 try:
225 matching_option = next(
224 matching_option = next(
226 (o for o in check.func.options if o[0] == option_name)
225 (o for o in check.func.options if o[0] == option_name)
227 )
226 )
228 except StopIteration:
227 except StopIteration:
229 raise error.InputError(
228 raise error.InputError(
230 _(b"check '%s' has no option '%s'") % (name, option_name)
229 _(b"check '%s' has no option '%s'") % (name, option_name)
231 )
230 )
232
231
233 # transform the argument from cli string to the expected Python type
232 # transform the argument from cli string to the expected Python type
234 _name, typ, _docstring = matching_option
233 _name, typ, _docstring = matching_option
235
234
236 as_typed = None
235 as_typed = None
237 if isinstance(typ, bool):
236 if isinstance(typ, bool):
238 as_bool = stringutil.parsebool(value)
237 as_bool = stringutil.parsebool(value)
239 if as_bool is None:
238 if as_bool is None:
240 raise error.InputError(
239 raise error.InputError(
241 _(b"'%s' is not a boolean ('%s')") % (path, value)
240 _(b"'%s' is not a boolean ('%s')") % (path, value)
242 )
241 )
243 as_typed = as_bool
242 as_typed = as_bool
244 elif isinstance(typ, list):
243 elif isinstance(typ, list):
245 as_list = stringutil.parselist(value)
244 as_list = stringutil.parselist(value)
246 if as_list is None:
245 if as_list is None:
247 raise error.InputError(
246 raise error.InputError(
248 _(b"'%s' is not a list ('%s')") % (path, value)
247 _(b"'%s' is not a list ('%s')") % (path, value)
249 )
248 )
250 as_typed = as_list
249 as_typed = as_list
251 else:
250 else:
252 raise error.ProgrammingError(b"unsupported type %s", type(typ))
251 raise error.ProgrammingError(b"unsupported type %s", type(typ))
253
252
254 if option_name in to_modify[name]:
253 if option_name in to_modify[name]:
255 raise error.InputError(
254 raise error.InputError(
256 _(b"duplicated option '%s' for '%s'") % (option_name, name)
255 _(b"duplicated option '%s' for '%s'") % (option_name, name)
257 )
256 )
258 else:
257 else:
259 assert as_typed is not None
258 assert as_typed is not None
260 to_modify[name][option_name] = as_typed
259 to_modify[name][option_name] = as_typed
261
260
262 # Manage case where a check is set but without command line options
261 # Manage case where a check is set but without command line options
263 # it will later be set with default check options values
262 # it will later be set with default check options values
264 for name, f in checks.items():
263 for name, f in checks.items():
265 if name not in to_modify:
264 if name not in to_modify:
266 to_modify[name] = {}
265 to_modify[name] = {}
267
266
268 # Merge default options with command line options
267 # Merge default options with command line options
269 for check_name, cmd_options in to_modify.items():
268 for check_name, cmd_options in to_modify.items():
270 check = checks.get(check_name)
269 check = checks.get(check_name)
271 func = checks[check_name]
270 func = checks[check_name]
272 merged_options = {}
271 merged_options = {}
273 # help Pytype
272 # help Pytype
274 assert check is not None
273 assert check is not None
275 assert check.func is not None
274 assert check.func is not None
276 assert hasattr(check.func, "options")
275 assert hasattr(check.func, "options")
277
276
278 if check.func.options:
277 if check.func.options:
279 # copy the default value in case it's mutable (list, etc.)
278 # copy the default value in case it's mutable (list, etc.)
280 merged_options = {
279 merged_options = {
281 o[0]: copy.deepcopy(o[1]) for o in check.func.options
280 o[0]: copy.deepcopy(o[1]) for o in check.func.options
282 }
281 }
283 if cmd_options:
282 if cmd_options:
284 for k, v in cmd_options.items():
283 for k, v in cmd_options.items():
285 merged_options[k] = v
284 merged_options[k] = v
286 options = pycompat.strkwargs(merged_options)
285 options = pycompat.strkwargs(merged_options)
287 checks[check_name] = functools.partial(func, **options)
286 checks[check_name] = functools.partial(func, **options)
288 ui.debug(b"merged options for '%s': '%r'\n" % (check_name, options))
287 ui.debug(b"merged options for '%s': '%r'\n" % (check_name, options))
289
288
290 return checks
289 return checks
291
290
292
291
293 def get_checks(
292 def get_checks(
294 repo,
293 repo,
295 ui,
294 ui,
296 names=None,
295 names=None,
297 options=None,
296 options=None,
298 table=None,
297 table=None,
299 alias_table=None,
298 alias_table=None,
300 full_pyramid=None,
299 full_pyramid=None,
301 ):
300 ):
302 """Given a list of function names and optionally a list of
301 """Given a list of function names and optionally a list of
303 options, return matched checks with merged options (command line options
302 options, return matched checks with merged options (command line options
304 values take precedence on default ones)
303 values take precedence on default ones)
305
304
306 It runs find checks, then resolve options and returns a dict of matched
305 It runs find checks, then resolve options and returns a dict of matched
307 functions with resolved options.
306 functions with resolved options.
308 """
307 """
309 funcs = {}
308 funcs = {}
310
309
311 if names is None:
310 if names is None:
312 names = []
311 names = []
313
312
314 if options is None:
313 if options is None:
315 options = []
314 options = []
316
315
317 # find checks
316 # find checks
318 for name in names:
317 for name in names:
319 matched = find_checks(
318 matched = find_checks(
320 name,
319 name,
321 table=table,
320 table=table,
322 alias_table=alias_table,
321 alias_table=alias_table,
323 full_pyramid=full_pyramid,
322 full_pyramid=full_pyramid,
324 )
323 )
325 matched_names = b", ".join(matched)
324 matched_names = b", ".join(matched)
326 ui.debug(b"found checks '%s' for name '%s'\n" % (matched_names, name))
325 ui.debug(b"found checks '%s' for name '%s'\n" % (matched_names, name))
327 funcs.update(matched)
326 funcs.update(matched)
328
327
329 funcs = {n: functools.partial(f, ui, repo) for n, f in funcs.items()}
328 funcs = {n: functools.partial(f, ui, repo) for n, f in funcs.items()}
330
329
331 # resolve options
330 # resolve options
332 checks = pass_options(
331 checks = pass_options(
333 ui,
332 ui,
334 funcs,
333 funcs,
335 options,
334 options,
336 table=table,
335 table=table,
337 alias_table=alias_table,
336 alias_table=alias_table,
338 full_pyramid=full_pyramid,
337 full_pyramid=full_pyramid,
339 )
338 )
340
339
341 return checks
340 return checks
General Comments 0
You need to be logged in to leave comments. Login now