##// END OF EJS Templates
branching: merge stable into default...
marmoute -
r51069:596a6b9b merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,33 b''
1 #!/bin/bash
2
3 set -e
4 set -u
5
6 # Find the python3 setup that would run pytype
7 PYTYPE=`which pytype`
8 PYTHON3=`head -n1 ${PYTYPE} | sed -s 's/#!//'`
9
10 # Existing stubs that pytype processes live here
11 TYPESHED=$(${PYTHON3} -c "import pytype; print(pytype.__path__[0])")/typeshed/stubs
12 HG_STUBS=${TYPESHED}/mercurial
13
14 echo "Patching typeshed at $HG_STUBS"
15
16 rm -rf ${HG_STUBS}
17 mkdir -p ${HG_STUBS}
18
19 cat > ${HG_STUBS}/METADATA.toml <<EOF
20 version = "0.1"
21 EOF
22
23
24 mkdir -p ${HG_STUBS}/mercurial/cext ${HG_STUBS}/mercurial/thirdparty/attr
25
26 touch ${HG_STUBS}/mercurial/__init__.pyi
27 touch ${HG_STUBS}/mercurial/cext/__init__.pyi
28 touch ${HG_STUBS}/mercurial/thirdparty/__init__.pyi
29
30 ln -sf $(hg root)/mercurial/cext/*.{pyi,typed} \
31 ${HG_STUBS}/mercurial/cext
32 ln -sf $(hg root)/mercurial/thirdparty/attr/*.{pyi,typed} \
33 ${HG_STUBS}/mercurial/thirdparty/attr
@@ -0,0 +1,1 b''
1 partial
@@ -0,0 +1,486 b''
1 import sys
2
3 from typing import (
4 Any,
5 Callable,
6 ClassVar,
7 Dict,
8 Generic,
9 List,
10 Mapping,
11 Optional,
12 Protocol,
13 Sequence,
14 Tuple,
15 Type,
16 TypeVar,
17 Union,
18 overload,
19 )
20
21 # `import X as X` is required to make these public
22 from . import converters as converters
23 from . import exceptions as exceptions
24 from . import filters as filters
25 from . import setters as setters
26 from . import validators as validators
27 from ._cmp import cmp_using as cmp_using
28 from ._version_info import VersionInfo
29
30 __version__: str
31 __version_info__: VersionInfo
32 __title__: str
33 __description__: str
34 __url__: str
35 __uri__: str
36 __author__: str
37 __email__: str
38 __license__: str
39 __copyright__: str
40
41 _T = TypeVar("_T")
42 _C = TypeVar("_C", bound=type)
43
44 _EqOrderType = Union[bool, Callable[[Any], Any]]
45 _ValidatorType = Callable[[Any, Attribute[_T], _T], Any]
46 _ConverterType = Callable[[Any], Any]
47 _FilterType = Callable[[Attribute[_T], _T], bool]
48 _ReprType = Callable[[Any], str]
49 _ReprArgType = Union[bool, _ReprType]
50 _OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]
51 _OnSetAttrArgType = Union[
52 _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType
53 ]
54 _FieldTransformer = Callable[
55 [type, List[Attribute[Any]]], List[Attribute[Any]]
56 ]
57 # FIXME: in reality, if multiple validators are passed they must be in a list
58 # or tuple, but those are invariant and so would prevent subtypes of
59 # _ValidatorType from working when passed in a list or tuple.
60 _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
61
62 # A protocol to be able to statically accept an attrs class.
63 class AttrsInstance(Protocol):
64 __attrs_attrs__: ClassVar[Any]
65
66 # _make --
67
68 NOTHING: object
69
70 # NOTE: Factory lies about its return type to make this possible:
71 # `x: List[int] # = Factory(list)`
72 # Work around mypy issue #4554 in the common case by using an overload.
73 if sys.version_info >= (3, 8):
74 from typing import Literal
75 @overload
76 def Factory(factory: Callable[[], _T]) -> _T: ...
77 @overload
78 def Factory(
79 factory: Callable[[Any], _T],
80 takes_self: Literal[True],
81 ) -> _T: ...
82 @overload
83 def Factory(
84 factory: Callable[[], _T],
85 takes_self: Literal[False],
86 ) -> _T: ...
87
88 else:
89 @overload
90 def Factory(factory: Callable[[], _T]) -> _T: ...
91 @overload
92 def Factory(
93 factory: Union[Callable[[Any], _T], Callable[[], _T]],
94 takes_self: bool = ...,
95 ) -> _T: ...
96
97 # Static type inference support via __dataclass_transform__ implemented as per:
98 # https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md
99 # This annotation must be applied to all overloads of "define" and "attrs"
100 #
101 # NOTE: This is a typing construct and does not exist at runtime. Extensions
102 # wrapping attrs decorators should declare a separate __dataclass_transform__
103 # signature in the extension module using the specification linked above to
104 # provide pyright support.
105 def __dataclass_transform__(
106 *,
107 eq_default: bool = True,
108 order_default: bool = False,
109 kw_only_default: bool = False,
110 field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
111 ) -> Callable[[_T], _T]: ...
112
113 class Attribute(Generic[_T]):
114 name: str
115 default: Optional[_T]
116 validator: Optional[_ValidatorType[_T]]
117 repr: _ReprArgType
118 cmp: _EqOrderType
119 eq: _EqOrderType
120 order: _EqOrderType
121 hash: Optional[bool]
122 init: bool
123 converter: Optional[_ConverterType]
124 metadata: Dict[Any, Any]
125 type: Optional[Type[_T]]
126 kw_only: bool
127 on_setattr: _OnSetAttrType
128 def evolve(self, **changes: Any) -> "Attribute[Any]": ...
129
130 # NOTE: We had several choices for the annotation to use for type arg:
131 # 1) Type[_T]
132 # - Pros: Handles simple cases correctly
133 # - Cons: Might produce less informative errors in the case of conflicting
134 # TypeVars e.g. `attr.ib(default='bad', type=int)`
135 # 2) Callable[..., _T]
136 # - Pros: Better error messages than #1 for conflicting TypeVars
137 # - Cons: Terrible error messages for validator checks.
138 # e.g. attr.ib(type=int, validator=validate_str)
139 # -> error: Cannot infer function type argument
140 # 3) type (and do all of the work in the mypy plugin)
141 # - Pros: Simple here, and we could customize the plugin with our own errors.
142 # - Cons: Would need to write mypy plugin code to handle all the cases.
143 # We chose option #1.
144
145 # `attr` lies about its return type to make the following possible:
146 # attr() -> Any
147 # attr(8) -> int
148 # attr(validator=<some callable>) -> Whatever the callable expects.
149 # This makes this type of assignments possible:
150 # x: int = attr(8)
151 #
152 # This form catches explicit None or no default but with no other arguments
153 # returns Any.
154 @overload
155 def attrib(
156 default: None = ...,
157 validator: None = ...,
158 repr: _ReprArgType = ...,
159 cmp: Optional[_EqOrderType] = ...,
160 hash: Optional[bool] = ...,
161 init: bool = ...,
162 metadata: Optional[Mapping[Any, Any]] = ...,
163 type: None = ...,
164 converter: None = ...,
165 factory: None = ...,
166 kw_only: bool = ...,
167 eq: Optional[_EqOrderType] = ...,
168 order: Optional[_EqOrderType] = ...,
169 on_setattr: Optional[_OnSetAttrArgType] = ...,
170 ) -> Any: ...
171
172 # This form catches an explicit None or no default and infers the type from the
173 # other arguments.
174 @overload
175 def attrib(
176 default: None = ...,
177 validator: Optional[_ValidatorArgType[_T]] = ...,
178 repr: _ReprArgType = ...,
179 cmp: Optional[_EqOrderType] = ...,
180 hash: Optional[bool] = ...,
181 init: bool = ...,
182 metadata: Optional[Mapping[Any, Any]] = ...,
183 type: Optional[Type[_T]] = ...,
184 converter: Optional[_ConverterType] = ...,
185 factory: Optional[Callable[[], _T]] = ...,
186 kw_only: bool = ...,
187 eq: Optional[_EqOrderType] = ...,
188 order: Optional[_EqOrderType] = ...,
189 on_setattr: Optional[_OnSetAttrArgType] = ...,
190 ) -> _T: ...
191
192 # This form catches an explicit default argument.
193 @overload
194 def attrib(
195 default: _T,
196 validator: Optional[_ValidatorArgType[_T]] = ...,
197 repr: _ReprArgType = ...,
198 cmp: Optional[_EqOrderType] = ...,
199 hash: Optional[bool] = ...,
200 init: bool = ...,
201 metadata: Optional[Mapping[Any, Any]] = ...,
202 type: Optional[Type[_T]] = ...,
203 converter: Optional[_ConverterType] = ...,
204 factory: Optional[Callable[[], _T]] = ...,
205 kw_only: bool = ...,
206 eq: Optional[_EqOrderType] = ...,
207 order: Optional[_EqOrderType] = ...,
208 on_setattr: Optional[_OnSetAttrArgType] = ...,
209 ) -> _T: ...
210
211 # This form covers type=non-Type: e.g. forward references (str), Any
212 @overload
213 def attrib(
214 default: Optional[_T] = ...,
215 validator: Optional[_ValidatorArgType[_T]] = ...,
216 repr: _ReprArgType = ...,
217 cmp: Optional[_EqOrderType] = ...,
218 hash: Optional[bool] = ...,
219 init: bool = ...,
220 metadata: Optional[Mapping[Any, Any]] = ...,
221 type: object = ...,
222 converter: Optional[_ConverterType] = ...,
223 factory: Optional[Callable[[], _T]] = ...,
224 kw_only: bool = ...,
225 eq: Optional[_EqOrderType] = ...,
226 order: Optional[_EqOrderType] = ...,
227 on_setattr: Optional[_OnSetAttrArgType] = ...,
228 ) -> Any: ...
229 @overload
230 def field(
231 *,
232 default: None = ...,
233 validator: None = ...,
234 repr: _ReprArgType = ...,
235 hash: Optional[bool] = ...,
236 init: bool = ...,
237 metadata: Optional[Mapping[Any, Any]] = ...,
238 converter: None = ...,
239 factory: None = ...,
240 kw_only: bool = ...,
241 eq: Optional[bool] = ...,
242 order: Optional[bool] = ...,
243 on_setattr: Optional[_OnSetAttrArgType] = ...,
244 ) -> Any: ...
245
246 # This form catches an explicit None or no default and infers the type from the
247 # other arguments.
248 @overload
249 def field(
250 *,
251 default: None = ...,
252 validator: Optional[_ValidatorArgType[_T]] = ...,
253 repr: _ReprArgType = ...,
254 hash: Optional[bool] = ...,
255 init: bool = ...,
256 metadata: Optional[Mapping[Any, Any]] = ...,
257 converter: Optional[_ConverterType] = ...,
258 factory: Optional[Callable[[], _T]] = ...,
259 kw_only: bool = ...,
260 eq: Optional[_EqOrderType] = ...,
261 order: Optional[_EqOrderType] = ...,
262 on_setattr: Optional[_OnSetAttrArgType] = ...,
263 ) -> _T: ...
264
265 # This form catches an explicit default argument.
266 @overload
267 def field(
268 *,
269 default: _T,
270 validator: Optional[_ValidatorArgType[_T]] = ...,
271 repr: _ReprArgType = ...,
272 hash: Optional[bool] = ...,
273 init: bool = ...,
274 metadata: Optional[Mapping[Any, Any]] = ...,
275 converter: Optional[_ConverterType] = ...,
276 factory: Optional[Callable[[], _T]] = ...,
277 kw_only: bool = ...,
278 eq: Optional[_EqOrderType] = ...,
279 order: Optional[_EqOrderType] = ...,
280 on_setattr: Optional[_OnSetAttrArgType] = ...,
281 ) -> _T: ...
282
283 # This form covers type=non-Type: e.g. forward references (str), Any
284 @overload
285 def field(
286 *,
287 default: Optional[_T] = ...,
288 validator: Optional[_ValidatorArgType[_T]] = ...,
289 repr: _ReprArgType = ...,
290 hash: Optional[bool] = ...,
291 init: bool = ...,
292 metadata: Optional[Mapping[Any, Any]] = ...,
293 converter: Optional[_ConverterType] = ...,
294 factory: Optional[Callable[[], _T]] = ...,
295 kw_only: bool = ...,
296 eq: Optional[_EqOrderType] = ...,
297 order: Optional[_EqOrderType] = ...,
298 on_setattr: Optional[_OnSetAttrArgType] = ...,
299 ) -> Any: ...
300 @overload
301 @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
302 def attrs(
303 maybe_cls: _C,
304 these: Optional[Dict[str, Any]] = ...,
305 repr_ns: Optional[str] = ...,
306 repr: bool = ...,
307 cmp: Optional[_EqOrderType] = ...,
308 hash: Optional[bool] = ...,
309 init: bool = ...,
310 slots: bool = ...,
311 frozen: bool = ...,
312 weakref_slot: bool = ...,
313 str: bool = ...,
314 auto_attribs: bool = ...,
315 kw_only: bool = ...,
316 cache_hash: bool = ...,
317 auto_exc: bool = ...,
318 eq: Optional[_EqOrderType] = ...,
319 order: Optional[_EqOrderType] = ...,
320 auto_detect: bool = ...,
321 collect_by_mro: bool = ...,
322 getstate_setstate: Optional[bool] = ...,
323 on_setattr: Optional[_OnSetAttrArgType] = ...,
324 field_transformer: Optional[_FieldTransformer] = ...,
325 match_args: bool = ...,
326 ) -> _C: ...
327 @overload
328 @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
329 def attrs(
330 maybe_cls: None = ...,
331 these: Optional[Dict[str, Any]] = ...,
332 repr_ns: Optional[str] = ...,
333 repr: bool = ...,
334 cmp: Optional[_EqOrderType] = ...,
335 hash: Optional[bool] = ...,
336 init: bool = ...,
337 slots: bool = ...,
338 frozen: bool = ...,
339 weakref_slot: bool = ...,
340 str: bool = ...,
341 auto_attribs: bool = ...,
342 kw_only: bool = ...,
343 cache_hash: bool = ...,
344 auto_exc: bool = ...,
345 eq: Optional[_EqOrderType] = ...,
346 order: Optional[_EqOrderType] = ...,
347 auto_detect: bool = ...,
348 collect_by_mro: bool = ...,
349 getstate_setstate: Optional[bool] = ...,
350 on_setattr: Optional[_OnSetAttrArgType] = ...,
351 field_transformer: Optional[_FieldTransformer] = ...,
352 match_args: bool = ...,
353 ) -> Callable[[_C], _C]: ...
354 @overload
355 @__dataclass_transform__(field_descriptors=(attrib, field))
356 def define(
357 maybe_cls: _C,
358 *,
359 these: Optional[Dict[str, Any]] = ...,
360 repr: bool = ...,
361 hash: Optional[bool] = ...,
362 init: bool = ...,
363 slots: bool = ...,
364 frozen: bool = ...,
365 weakref_slot: bool = ...,
366 str: bool = ...,
367 auto_attribs: bool = ...,
368 kw_only: bool = ...,
369 cache_hash: bool = ...,
370 auto_exc: bool = ...,
371 eq: Optional[bool] = ...,
372 order: Optional[bool] = ...,
373 auto_detect: bool = ...,
374 getstate_setstate: Optional[bool] = ...,
375 on_setattr: Optional[_OnSetAttrArgType] = ...,
376 field_transformer: Optional[_FieldTransformer] = ...,
377 match_args: bool = ...,
378 ) -> _C: ...
379 @overload
380 @__dataclass_transform__(field_descriptors=(attrib, field))
381 def define(
382 maybe_cls: None = ...,
383 *,
384 these: Optional[Dict[str, Any]] = ...,
385 repr: bool = ...,
386 hash: Optional[bool] = ...,
387 init: bool = ...,
388 slots: bool = ...,
389 frozen: bool = ...,
390 weakref_slot: bool = ...,
391 str: bool = ...,
392 auto_attribs: bool = ...,
393 kw_only: bool = ...,
394 cache_hash: bool = ...,
395 auto_exc: bool = ...,
396 eq: Optional[bool] = ...,
397 order: Optional[bool] = ...,
398 auto_detect: bool = ...,
399 getstate_setstate: Optional[bool] = ...,
400 on_setattr: Optional[_OnSetAttrArgType] = ...,
401 field_transformer: Optional[_FieldTransformer] = ...,
402 match_args: bool = ...,
403 ) -> Callable[[_C], _C]: ...
404
405 mutable = define
406 frozen = define # they differ only in their defaults
407
408 def fields(cls: Type[AttrsInstance]) -> Any: ...
409 def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ...
410 def validate(inst: AttrsInstance) -> None: ...
411 def resolve_types(
412 cls: _C,
413 globalns: Optional[Dict[str, Any]] = ...,
414 localns: Optional[Dict[str, Any]] = ...,
415 attribs: Optional[List[Attribute[Any]]] = ...,
416 ) -> _C: ...
417
418 # TODO: add support for returning a proper attrs class from the mypy plugin
419 # we use Any instead of _CountingAttr so that e.g. `make_class('Foo',
420 # [attr.ib()])` is valid
421 def make_class(
422 name: str,
423 attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]],
424 bases: Tuple[type, ...] = ...,
425 repr_ns: Optional[str] = ...,
426 repr: bool = ...,
427 cmp: Optional[_EqOrderType] = ...,
428 hash: Optional[bool] = ...,
429 init: bool = ...,
430 slots: bool = ...,
431 frozen: bool = ...,
432 weakref_slot: bool = ...,
433 str: bool = ...,
434 auto_attribs: bool = ...,
435 kw_only: bool = ...,
436 cache_hash: bool = ...,
437 auto_exc: bool = ...,
438 eq: Optional[_EqOrderType] = ...,
439 order: Optional[_EqOrderType] = ...,
440 collect_by_mro: bool = ...,
441 on_setattr: Optional[_OnSetAttrArgType] = ...,
442 field_transformer: Optional[_FieldTransformer] = ...,
443 ) -> type: ...
444
445 # _funcs --
446
447 # TODO: add support for returning TypedDict from the mypy plugin
448 # FIXME: asdict/astuple do not honor their factory args. Waiting on one of
449 # these:
450 # https://github.com/python/mypy/issues/4236
451 # https://github.com/python/typing/issues/253
452 # XXX: remember to fix attrs.asdict/astuple too!
453 def asdict(
454 inst: AttrsInstance,
455 recurse: bool = ...,
456 filter: Optional[_FilterType[Any]] = ...,
457 dict_factory: Type[Mapping[Any, Any]] = ...,
458 retain_collection_types: bool = ...,
459 value_serializer: Optional[
460 Callable[[type, Attribute[Any], Any], Any]
461 ] = ...,
462 tuple_keys: Optional[bool] = ...,
463 ) -> Dict[str, Any]: ...
464
465 # TODO: add support for returning NamedTuple from the mypy plugin
466 def astuple(
467 inst: AttrsInstance,
468 recurse: bool = ...,
469 filter: Optional[_FilterType[Any]] = ...,
470 tuple_factory: Type[Sequence[Any]] = ...,
471 retain_collection_types: bool = ...,
472 ) -> Tuple[Any, ...]: ...
473 def has(cls: type) -> bool: ...
474 def assoc(inst: _T, **changes: Any) -> _T: ...
475 def evolve(inst: _T, **changes: Any) -> _T: ...
476
477 # _config --
478
479 def set_run_validators(run: bool) -> None: ...
480 def get_run_validators() -> bool: ...
481
482 # aliases --
483
484 s = attributes = attrs
485 ib = attr = attrib
486 dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;)
@@ -0,0 +1,155 b''
1 # SPDX-License-Identifier: MIT
2
3
4 import functools
5 import types
6
7 from ._make import _make_ne
8
9
10 _operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
11
12
13 def cmp_using(
14 eq=None,
15 lt=None,
16 le=None,
17 gt=None,
18 ge=None,
19 require_same_type=True,
20 class_name="Comparable",
21 ):
22 """
23 Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and
24 ``cmp`` arguments to customize field comparison.
25
26 The resulting class will have a full set of ordering methods if
27 at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided.
28
29 :param Optional[callable] eq: `callable` used to evaluate equality
30 of two objects.
31 :param Optional[callable] lt: `callable` used to evaluate whether
32 one object is less than another object.
33 :param Optional[callable] le: `callable` used to evaluate whether
34 one object is less than or equal to another object.
35 :param Optional[callable] gt: `callable` used to evaluate whether
36 one object is greater than another object.
37 :param Optional[callable] ge: `callable` used to evaluate whether
38 one object is greater than or equal to another object.
39
40 :param bool require_same_type: When `True`, equality and ordering methods
41 will return `NotImplemented` if objects are not of the same type.
42
43 :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.
44
45 See `comparison` for more details.
46
47 .. versionadded:: 21.1.0
48 """
49
50 body = {
51 "__slots__": ["value"],
52 "__init__": _make_init(),
53 "_requirements": [],
54 "_is_comparable_to": _is_comparable_to,
55 }
56
57 # Add operations.
58 num_order_functions = 0
59 has_eq_function = False
60
61 if eq is not None:
62 has_eq_function = True
63 body["__eq__"] = _make_operator("eq", eq)
64 body["__ne__"] = _make_ne()
65
66 if lt is not None:
67 num_order_functions += 1
68 body["__lt__"] = _make_operator("lt", lt)
69
70 if le is not None:
71 num_order_functions += 1
72 body["__le__"] = _make_operator("le", le)
73
74 if gt is not None:
75 num_order_functions += 1
76 body["__gt__"] = _make_operator("gt", gt)
77
78 if ge is not None:
79 num_order_functions += 1
80 body["__ge__"] = _make_operator("ge", ge)
81
82 type_ = types.new_class(
83 class_name, (object,), {}, lambda ns: ns.update(body)
84 )
85
86 # Add same type requirement.
87 if require_same_type:
88 type_._requirements.append(_check_same_type)
89
90 # Add total ordering if at least one operation was defined.
91 if 0 < num_order_functions < 4:
92 if not has_eq_function:
93 # functools.total_ordering requires __eq__ to be defined,
94 # so raise early error here to keep a nice stack.
95 raise ValueError(
96 "eq must be define is order to complete ordering from "
97 "lt, le, gt, ge."
98 )
99 type_ = functools.total_ordering(type_)
100
101 return type_
102
103
104 def _make_init():
105 """
106 Create __init__ method.
107 """
108
109 def __init__(self, value):
110 """
111 Initialize object with *value*.
112 """
113 self.value = value
114
115 return __init__
116
117
118 def _make_operator(name, func):
119 """
120 Create operator method.
121 """
122
123 def method(self, other):
124 if not self._is_comparable_to(other):
125 return NotImplemented
126
127 result = func(self.value, other.value)
128 if result is NotImplemented:
129 return NotImplemented
130
131 return result
132
133 method.__name__ = "__%s__" % (name,)
134 method.__doc__ = "Return a %s b. Computed by attrs." % (
135 _operation_names[name],
136 )
137
138 return method
139
140
141 def _is_comparable_to(self, other):
142 """
143 Check whether `other` is comparable to `self`.
144 """
145 for func in self._requirements:
146 if not func(self, other):
147 return False
148 return True
149
150
151 def _check_same_type(self, other):
152 """
153 Return True if *self* and *other* are of the same type, False otherwise.
154 """
155 return other.value.__class__ is self.value.__class__
@@ -0,0 +1,13 b''
1 from typing import Any, Callable, Optional, Type
2
3 _CompareWithType = Callable[[Any, Any], bool]
4
5 def cmp_using(
6 eq: Optional[_CompareWithType],
7 lt: Optional[_CompareWithType],
8 le: Optional[_CompareWithType],
9 gt: Optional[_CompareWithType],
10 ge: Optional[_CompareWithType],
11 require_same_type: bool,
12 class_name: str,
13 ) -> Type: ...
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,5 +1,8 b''
1 1 /assign_reviewer @mercurial.review
2 2
3
4 <!--
5
3 6 Welcome to the Mercurial Merge Request creation process:
4 7
5 8 * Set a simple title for your MR,
@@ -11,3 +14,5 b' More details here:'
11 14
12 15 * https://www.mercurial-scm.org/wiki/ContributingChanges
13 16 * https://www.mercurial-scm.org/wiki/Heptapod
17
18 -->
@@ -138,6 +138,7 b' tests:'
138 138 # Run Rust tests if cargo is installed
139 139 if command -v $(CARGO) >/dev/null 2>&1; then \
140 140 $(MAKE) rust-tests; \
141 $(MAKE) cargo-clippy; \
141 142 fi
142 143 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
143 144
@@ -152,9 +153,13 b' testpy-%:'
152 153 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
153 154
154 155 rust-tests:
155 cd $(HGROOT)/rust/hg-cpython \
156 cd $(HGROOT)/rust \
156 157 && $(CARGO) test --quiet --all --features "$(HG_RUST_FEATURES)"
157 158
159 cargo-clippy:
160 cd $(HGROOT)/rust \
161 && $(CARGO) clippy --all --features "$(HG_RUST_FEATURES)" -- -D warnings
162
158 163 check-code:
159 164 hg manifest | xargs python contrib/check-code.py
160 165
@@ -372,10 +372,6 b' commonpypats = ['
372 372 ),
373 373 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
374 374 (
375 r'\([^()]*( =[^=]|[^<>!=]= )',
376 "no whitespace around = for named parameters",
377 ),
378 (
379 375 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
380 376 "don't use old-style two-argument raise, use Exception(message)",
381 377 ),
@@ -12,6 +12,36 b' cd `hg root`'
12 12 # endeavor to empty this list out over time, as some of these are
13 13 # probably hiding real problems.
14 14 #
15 # hgext/absorb.py # [attribute-error]
16 # hgext/bugzilla.py # [pyi-error], [attribute-error]
17 # hgext/convert/bzr.py # [attribute-error]
18 # hgext/convert/cvs.py # [attribute-error], [wrong-arg-types]
19 # hgext/convert/cvsps.py # [attribute-error]
20 # hgext/convert/p4.py # [wrong-arg-types] (__file: mercurial.utils.procutil._pfile -> IO)
21 # hgext/convert/subversion.py # [attribute-error], [name-error], [pyi-error]
22 # hgext/fastannotate/context.py # no linelog.copyfrom()
23 # hgext/fastannotate/formatter.py # [unsupported-operands]
24 # hgext/fsmonitor/__init__.py # [name-error]
25 # hgext/git/__init__.py # [attribute-error]
26 # hgext/githelp.py # [attribute-error] [wrong-arg-types]
27 # hgext/hgk.py # [attribute-error]
28 # hgext/histedit.py # [attribute-error], [wrong-arg-types]
29 # hgext/infinitepush # using bytes for str literal; scheduled for removal
30 # hgext/keyword.py # [attribute-error]
31 # hgext/largefiles/storefactory.py # [attribute-error]
32 # hgext/lfs/__init__.py # [attribute-error]
33 # hgext/narrow/narrowbundle2.py # [attribute-error]
34 # hgext/narrow/narrowcommands.py # [attribute-error], [name-error]
35 # hgext/rebase.py # [attribute-error]
36 # hgext/remotefilelog/basepack.py # [attribute-error], [wrong-arg-count]
37 # hgext/remotefilelog/basestore.py # [attribute-error]
38 # hgext/remotefilelog/contentstore.py # [missing-parameter], [wrong-keyword-args], [attribute-error]
39 # hgext/remotefilelog/fileserverclient.py # [attribute-error]
40 # hgext/remotefilelog/shallowbundle.py # [attribute-error]
41 # hgext/remotefilelog/remotefilectx.py # [module-attr] (This is an actual bug)
42 # hgext/sqlitestore.py # [attribute-error]
43 # hgext/zeroconf/__init__.py # bytes vs str; tests fail on macOS
44 #
15 45 # mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
16 46 # mercurial/context.py # many [attribute-error]
17 47 # mercurial/crecord.py # tons of [attribute-error], [module-attr]
@@ -31,7 +61,6 b' cd `hg root`'
31 61 # mercurial/pure/parsers.py # [attribute-error]
32 62 # mercurial/repoview.py # [attribute-error]
33 63 # mercurial/testing/storage.py # tons of [attribute-error]
34 # mercurial/ui.py # [attribute-error], [wrong-arg-types]
35 64 # mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
36 65 # mercurial/win32.py # [not-callable]
37 66 # mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
@@ -43,7 +72,37 b' cd `hg root`'
43 72
44 73 # TODO: include hgext and hgext3rd
45 74
46 pytype -V 3.7 --keep-going --jobs auto mercurial \
75 pytype -V 3.7 --keep-going --jobs auto \
76 doc/check-seclevel.py hgdemandimport hgext mercurial \
77 -x hgext/absorb.py \
78 -x hgext/bugzilla.py \
79 -x hgext/convert/bzr.py \
80 -x hgext/convert/cvs.py \
81 -x hgext/convert/cvsps.py \
82 -x hgext/convert/p4.py \
83 -x hgext/convert/subversion.py \
84 -x hgext/fastannotate/context.py \
85 -x hgext/fastannotate/formatter.py \
86 -x hgext/fsmonitor/__init__.py \
87 -x hgext/git/__init__.py \
88 -x hgext/githelp.py \
89 -x hgext/hgk.py \
90 -x hgext/histedit.py \
91 -x hgext/infinitepush \
92 -x hgext/keyword.py \
93 -x hgext/largefiles/storefactory.py \
94 -x hgext/lfs/__init__.py \
95 -x hgext/narrow/narrowbundle2.py \
96 -x hgext/narrow/narrowcommands.py \
97 -x hgext/rebase.py \
98 -x hgext/remotefilelog/basepack.py \
99 -x hgext/remotefilelog/basestore.py \
100 -x hgext/remotefilelog/contentstore.py \
101 -x hgext/remotefilelog/fileserverclient.py \
102 -x hgext/remotefilelog/remotefilectx.py \
103 -x hgext/remotefilelog/shallowbundle.py \
104 -x hgext/sqlitestore.py \
105 -x hgext/zeroconf/__init__.py \
47 106 -x mercurial/bundlerepo.py \
48 107 -x mercurial/context.py \
49 108 -x mercurial/crecord.py \
@@ -64,9 +123,11 b' pytype -V 3.7 --keep-going --jobs auto m'
64 123 -x mercurial/repoview.py \
65 124 -x mercurial/testing/storage.py \
66 125 -x mercurial/thirdparty \
67 -x mercurial/ui.py \
68 126 -x mercurial/unionrepo.py \
69 127 -x mercurial/win32.py \
70 128 -x mercurial/wireprotoframing.py \
71 129 -x mercurial/wireprotov1peer.py \
72 130 -x mercurial/wireprotov1server.py
131
132 echo 'pytype crashed while generating the following type stubs:'
133 find .pytype/pyi -name '*.pyi' | xargs grep -l '# Caught error' | sort
@@ -20,7 +20,7 b' for inline in (True, False):'
20 20 index, cache = parsers.parse_index2(data, inline)
21 21 index.slicechunktodensity(list(range(len(index))), 0.5, 262144)
22 22 index.stats()
23 index.findsnapshots({}, 0)
23 index.findsnapshots({}, 0, len(index) - 1)
24 24 10 in index
25 25 for rev in range(len(index)):
26 26 index.reachableroots(0, [len(index)-1], [rev])
@@ -42,6 +42,7 b' rust-cargo-test:'
42 42 script:
43 43 - echo "python used, $PYTHON"
44 44 - make rust-tests
45 - make cargo-clippy
45 46 variables:
46 47 PYTHON: python3
47 48 CI_CLEVER_CLOUD_FLAVOR: S
@@ -91,7 +92,8 b' check-pytype:'
91 92 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
92 93 - cd /tmp/mercurial-ci/
93 94 - make local PYTHON=$PYTHON
94 - $PYTHON -m pip install --user -U libcst==0.3.20 pytype==2022.03.29
95 - $PYTHON -m pip install --user -U libcst==0.3.20 pytype==2022.11.18
96 - ./contrib/setup-pytype.sh
95 97 script:
96 98 - echo "Entering script section"
97 99 - sh contrib/check-pytype.sh
@@ -235,6 +235,7 b' revlogopts = getattr('
235 235
236 236 cmdtable = {}
237 237
238
238 239 # for "historical portability":
239 240 # define parsealiases locally, because cmdutil.parsealiases has been
240 241 # available since 1.5 (or 6252852b4332)
@@ -573,7 +574,6 b' def _timer('
573 574
574 575
575 576 def formatone(fm, timings, title=None, result=None, displayall=False):
576
577 577 count = len(timings)
578 578
579 579 fm.startitem()
@@ -815,7 +815,12 b' def perfstatus(ui, repo, **opts):'
815 815 )
816 816 sum(map(bool, s))
817 817
818 timer(status_dirstate)
818 if util.safehasattr(dirstate, 'running_status'):
819 with dirstate.running_status(repo):
820 timer(status_dirstate)
821 dirstate.invalidate()
822 else:
823 timer(status_dirstate)
819 824 else:
820 825 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
821 826 fm.end()
@@ -997,11 +1002,16 b' def perfdiscovery(ui, repo, path, **opts'
997 1002 timer, fm = gettimer(ui, opts)
998 1003
999 1004 try:
1000 from mercurial.utils.urlutil import get_unique_pull_path
1001
1002 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
1005 from mercurial.utils.urlutil import get_unique_pull_path_obj
1006
1007 path = get_unique_pull_path_obj(b'perfdiscovery', ui, path)
1003 1008 except ImportError:
1004 path = ui.expandpath(path)
1009 try:
1010 from mercurial.utils.urlutil import get_unique_pull_path
1011
1012 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
1013 except ImportError:
1014 path = ui.expandpath(path)
1005 1015
1006 1016 def s():
1007 1017 repos[1] = hg.peer(ui, opts, path)
@@ -1469,7 +1479,8 b' def perfdirstatewrite(ui, repo, **opts):'
1469 1479 def d():
1470 1480 ds.write(repo.currenttransaction())
1471 1481
1472 timer(d, setup=setup)
1482 with repo.wlock():
1483 timer(d, setup=setup)
1473 1484 fm.end()
1474 1485
1475 1486
@@ -1613,7 +1624,11 b' def perfphasesremote(ui, repo, dest=None'
1613 1624 b'default repository not configured!',
1614 1625 hint=b"see 'hg help config.paths'",
1615 1626 )
1616 dest = path.pushloc or path.loc
1627 if util.safehasattr(path, 'main_path'):
1628 path = path.get_push_variant()
1629 dest = path.loc
1630 else:
1631 dest = path.pushloc or path.loc
1617 1632 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1618 1633 other = hg.peer(repo, opts, dest)
1619 1634
@@ -7,14 +7,12 b''
7 7
8 8
9 9 import abc
10 import builtins
10 11 import re
11 import sys
12 12
13 13 ####################
14 14 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
15 15
16 ispy3 = sys.version_info[0] >= 3
17
18 16
19 17 def identity(a):
20 18 return a
@@ -38,27 +36,19 b' def rapply(f, xs):'
38 36 return _rapply(f, xs)
39 37
40 38
41 if ispy3:
42 import builtins
43
44 def bytestr(s):
45 # tiny version of pycompat.bytestr
46 return s.encode('latin1')
47
48 def sysstr(s):
49 if isinstance(s, builtins.str):
50 return s
51 return s.decode('latin-1')
52
53 def opentext(f):
54 return open(f, 'r')
39 def bytestr(s):
40 # tiny version of pycompat.bytestr
41 return s.encode('latin1')
55 42
56 43
57 else:
58 bytestr = str
59 sysstr = identity
44 def sysstr(s):
45 if isinstance(s, builtins.str):
46 return s
47 return s.decode('latin-1')
60 48
61 opentext = open
49
50 def opentext(f):
51 return open(f, 'r')
62 52
63 53
64 54 def b2s(x):
@@ -46,7 +46,7 b' def showavailables(ui, initlevel):'
46 46
47 47
48 48 def checkseclevel(ui, doc, name, initlevel):
49 ui.notenoi18n('checking "%s"\n' % name)
49 ui.notenoi18n(('checking "%s"\n' % name).encode('utf-8'))
50 50 if not isinstance(doc, bytes):
51 51 doc = doc.encode('utf-8')
52 52 blocks, pruned = minirst.parse(doc, 0, ['verbose'])
@@ -70,14 +70,18 b' def checkseclevel(ui, doc, name, initlev'
70 70 nextlevel = mark2level[mark]
71 71 if curlevel < nextlevel and curlevel + 1 != nextlevel:
72 72 ui.warnnoi18n(
73 'gap of section level at "%s" of %s\n' % (title, name)
73 ('gap of section level at "%s" of %s\n' % (title, name)).encode(
74 'utf-8'
75 )
74 76 )
75 77 showavailables(ui, initlevel)
76 78 errorcnt += 1
77 79 continue
78 80 ui.notenoi18n(
79 'appropriate section level for "%s %s"\n'
80 % (mark * (nextlevel * 2), title)
81 (
82 'appropriate section level for "%s %s"\n'
83 % (mark * (nextlevel * 2), title)
84 ).encode('utf-8')
81 85 )
82 86 curlevel = nextlevel
83 87
@@ -90,7 +94,9 b' def checkcmdtable(ui, cmdtable, namefmt,'
90 94 name = k.split(b"|")[0].lstrip(b"^")
91 95 if not entry[0].__doc__:
92 96 ui.notenoi18n(
93 'skip checking %s: no help document\n' % (namefmt % name)
97 (
98 'skip checking %s: no help document\n' % (namefmt % name)
99 ).encode('utf-8')
94 100 )
95 101 continue
96 102 errorcnt += checkseclevel(
@@ -117,7 +123,9 b' def checkhghelps(ui):'
117 123 mod = extensions.load(ui, name, None)
118 124 if not mod.__doc__:
119 125 ui.notenoi18n(
120 'skip checking %s extension: no help document\n' % name
126 (
127 'skip checking %s extension: no help document\n' % name
128 ).encode('utf-8')
121 129 )
122 130 continue
123 131 errorcnt += checkseclevel(
@@ -144,7 +152,9 b' def checkfile(ui, filename, initlevel):'
144 152 doc = fp.read()
145 153
146 154 ui.notenoi18n(
147 'checking input from %s with initlevel %d\n' % (filename, initlevel)
155 (
156 'checking input from %s with initlevel %d\n' % (filename, initlevel)
157 ).encode('utf-8')
148 158 )
149 159 return checkseclevel(ui, doc, 'input from %s' % filename, initlevel)
150 160
@@ -23,8 +23,6 b' This also has some limitations compared '
23 23 enabled.
24 24 """
25 25
26 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
27
28 26 import contextlib
29 27 import importlib.util
30 28 import sys
@@ -39,10 +37,16 b' class _lazyloaderex(importlib.util.LazyL'
39 37 the ignore list.
40 38 """
41 39
40 _HAS_DYNAMIC_ATTRIBUTES = True # help pytype not flag self.loader
41
42 42 def exec_module(self, module):
43 43 """Make the module load lazily."""
44 44 with tracing.log('demandimport %s', module):
45 45 if _deactivated or module.__name__ in ignores:
46 # Reset the loader on the module as super() does (issue6725)
47 module.__spec__.loader = self.loader
48 module.__loader__ = self.loader
49
46 50 self.loader.exec_module(module)
47 51 else:
48 52 super().exec_module(module)
@@ -881,7 +881,7 b' class fixupstate:'
881 881
882 882 dirstate._fsmonitorstate.invalidate = noop
883 883 try:
884 with dirstate.parentchange():
884 with dirstate.changing_parents(self.repo):
885 885 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
886 886 finally:
887 887 restore()
@@ -46,6 +46,7 b' command = registrar.command(cmdtable)'
46 46 _(b'mark a branch as closed, hiding it from the branch list'),
47 47 ),
48 48 (b's', b'secret', None, _(b'use the secret phase for committing')),
49 (b'', b'draft', None, _(b'use the draft phase for committing')),
49 50 (b'n', b'note', b'', _(b'store a note on the amend')),
50 51 ]
51 52 + cmdutil.walkopts
@@ -64,6 +65,7 b' def amend(ui, repo, *pats, **opts):'
64 65
65 66 See :hg:`help commit` for more details.
66 67 """
68 cmdutil.check_at_most_one_arg(opts, 'draft', 'secret')
67 69 cmdutil.check_note_size(opts)
68 70
69 71 with repo.wlock(), repo.lock():
@@ -59,21 +59,29 b' def mvcheck(orig, ui, repo, *pats, **opt'
59 59 opts = pycompat.byteskwargs(opts)
60 60 renames = None
61 61 disabled = opts.pop(b'no_automv', False)
62 if not disabled:
63 threshold = ui.configint(b'automv', b'similarity')
64 if not 0 <= threshold <= 100:
65 raise error.Abort(_(b'automv.similarity must be between 0 and 100'))
66 if threshold > 0:
67 match = scmutil.match(repo[None], pats, opts)
68 added, removed = _interestingfiles(repo, match)
69 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
70 renames = _findrenames(
71 repo, uipathfn, added, removed, threshold / 100.0
72 )
62 with repo.wlock():
63 if not disabled:
64 threshold = ui.configint(b'automv', b'similarity')
65 if not 0 <= threshold <= 100:
66 raise error.Abort(
67 _(b'automv.similarity must be between 0 and 100')
68 )
69 if threshold > 0:
70 match = scmutil.match(repo[None], pats, opts)
71 added, removed = _interestingfiles(repo, match)
72 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
73 renames = _findrenames(
74 repo, uipathfn, added, removed, threshold / 100.0
75 )
73 76
74 with repo.wlock():
75 77 if renames is not None:
76 scmutil._markchanges(repo, (), (), renames)
78 with repo.dirstate.changing_files(repo):
79 # XXX this should be wider and integrated with the commit
80 # transaction. At the same time as we do the `addremove` logic
81 # for commit. However we can't really do better with the
82 # current extension structure, and this is not worse than what
83 # happened before.
84 scmutil._markchanges(repo, (), (), renames)
77 85 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
78 86
79 87
@@ -217,6 +217,8 b' def blackbox(ui, repo, *revs, **opts):'
217 217 return
218 218
219 219 limit = opts.get('limit')
220 assert limit is not None # help pytype
221
220 222 fp = repo.vfs(b'blackbox.log', b'r')
221 223 lines = fp.read().split(b'\n')
222 224
@@ -31,11 +31,14 b' demandimport.IGNORES.update('
31 31
32 32 try:
33 33 # bazaar imports
34 # pytype: disable=import-error
34 35 import breezy.bzr.bzrdir
35 36 import breezy.errors
36 37 import breezy.revision
37 38 import breezy.revisionspec
38 39
40 # pytype: enable=import-error
41
39 42 bzrdir = breezy.bzr.bzrdir
40 43 errors = breezy.errors
41 44 revision = breezy.revision
@@ -608,7 +608,10 b' class mercurial_source(common.converter_'
608 608 files = copyfiles = ctx.manifest()
609 609 if parents:
610 610 if self._changescache[0] == rev:
611 ma, r = self._changescache[1]
611 # TODO: add type hints to avoid this warning, instead of
612 # suppressing it:
613 # No attribute '__iter__' on None [attribute-error]
614 ma, r = self._changescache[1] # pytype: disable=attribute-error
612 615 else:
613 616 ma, r = self._changedfiles(parents[0], ctx)
614 617 if not full:
@@ -243,6 +243,7 b' class monotone_source(common.converter_s'
243 243 m = self.cert_re.match(e)
244 244 if m:
245 245 name, value = m.groups()
246 assert value is not None # help pytype
246 247 value = value.replace(br'\"', b'"')
247 248 value = value.replace(br'\\', b'\\')
248 249 certs[name] = value
@@ -47,11 +47,14 b' NoRepo = common.NoRepo'
47 47 # these bindings.
48 48
49 49 try:
50 # pytype: disable=import-error
50 51 import svn
51 52 import svn.client
52 53 import svn.core
53 54 import svn.ra
54 55 import svn.delta
56
57 # pytype: enable=import-error
55 58 from . import transport
56 59 import warnings
57 60
@@ -722,7 +725,13 b' class svn_source(converter_source):'
722 725 def getchanges(self, rev, full):
723 726 # reuse cache from getchangedfiles
724 727 if self._changescache[0] == rev and not full:
728 # TODO: add type hints to avoid this warning, instead of
729 # suppressing it:
730 # No attribute '__iter__' on None [attribute-error]
731
732 # pytype: disable=attribute-error
725 733 (files, copies) = self._changescache[1]
734 # pytype: enable=attribute-error
726 735 else:
727 736 (files, copies) = self._getchanges(rev, full)
728 737 # caller caches the result, so free it here to release memory
@@ -17,10 +17,13 b''
17 17 # You should have received a copy of the GNU General Public License
18 18 # along with this program; if not, see <http://www.gnu.org/licenses/>.
19 19
20 # pytype: disable=import-error
20 21 import svn.client
21 22 import svn.core
22 23 import svn.ra
23 24
25 # pytype: enable=import-error
26
24 27 Pool = svn.core.Pool
25 28 SubversionException = svn.core.SubversionException
26 29
@@ -37,7 +40,7 b' svn_config = None'
37 40
38 41 def _create_auth_baton(pool):
39 42 """Create a Subversion authentication baton."""
40 import svn.client
43 import svn.client # pytype: disable=import-error
41 44
42 45 # Give the client context baton a suite of authentication
43 46 # providers.h
@@ -421,30 +421,31 b' def reposetup(ui, repo):'
421 421 wlock = None
422 422 try:
423 423 wlock = self.wlock()
424 for f in self.dirstate:
425 if not self.dirstate.get_entry(f).maybe_clean:
426 continue
427 if oldeol is not None:
428 if not oldeol.match(f) and not neweol.match(f):
424 with self.dirstate.changing_files(self):
425 for f in self.dirstate:
426 if not self.dirstate.get_entry(f).maybe_clean:
429 427 continue
430 oldkey = None
431 for pattern, key, m in oldeol.patterns:
432 if m(f):
433 oldkey = key
434 break
435 newkey = None
436 for pattern, key, m in neweol.patterns:
437 if m(f):
438 newkey = key
439 break
440 if oldkey == newkey:
441 continue
442 # all normal files need to be looked at again since
443 # the new .hgeol file specify a different filter
444 self.dirstate.set_possibly_dirty(f)
445 # Write the cache to update mtime and cache .hgeol
446 with self.vfs(b"eol.cache", b"w") as f:
447 f.write(hgeoldata)
428 if oldeol is not None:
429 if not oldeol.match(f) and not neweol.match(f):
430 continue
431 oldkey = None
432 for pattern, key, m in oldeol.patterns:
433 if m(f):
434 oldkey = key
435 break
436 newkey = None
437 for pattern, key, m in neweol.patterns:
438 if m(f):
439 newkey = key
440 break
441 if oldkey == newkey:
442 continue
443 # all normal files need to be looked at again since
444 # the new .hgeol file specify a different filter
445 self.dirstate.set_possibly_dirty(f)
446 # Write the cache to update mtime and cache .hgeol
447 with self.vfs(b"eol.cache", b"w") as f:
448 f.write(hgeoldata)
448 449 except errormod.LockUnavailable:
449 450 # If we cannot lock the repository and clear the
450 451 # dirstate, then a commit might not see all files
@@ -151,8 +151,11 b' def annotatepeer(repo):'
151 151 ui = repo.ui
152 152
153 153 remotedest = ui.config(b'fastannotate', b'remotepath', b'default')
154 r = urlutil.get_unique_pull_path(b'fastannotate', repo, ui, remotedest)
155 remotepath = r[0]
154 remotepath = urlutil.get_unique_pull_path_obj(
155 b'fastannotate',
156 ui,
157 remotedest,
158 )
156 159 peer = hg.peer(ui, {}, remotepath)
157 160
158 161 try:
@@ -108,9 +108,9 b" def fetch(ui, repo, source=b'default', *"
108 108 )
109 109 )
110 110
111 path = urlutil.get_unique_pull_path(b'fetch', repo, ui, source)[0]
111 path = urlutil.get_unique_pull_path_obj(b'fetch', ui, source)
112 112 other = hg.peer(repo, opts, path)
113 ui.status(_(b'pulling from %s\n') % urlutil.hidepassword(path))
113 ui.status(_(b'pulling from %s\n') % urlutil.hidepassword(path.loc))
114 114 revs = None
115 115 if opts[b'rev']:
116 116 try:
@@ -779,7 +779,7 b' def writeworkingdir(repo, ctx, filedata,'
779 779 newp1 = replacements.get(oldp1, oldp1)
780 780 if newp1 != oldp1:
781 781 assert repo.dirstate.p2() == nullid
782 with repo.dirstate.parentchange():
782 with repo.dirstate.changing_parents(repo):
783 783 scmutil.movedirstate(repo, repo[newp1])
784 784
785 785
@@ -26,8 +26,6 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29 import inspect
32 30 import math
33 31 import os
@@ -94,7 +92,9 b' if os.name == "nt":'
94 92
95 93 LPDWORD = ctypes.POINTER(wintypes.DWORD)
96 94
97 CreateFile = ctypes.windll.kernel32.CreateFileA
95 _kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
96
97 CreateFile = _kernel32.CreateFileA
98 98 CreateFile.argtypes = [
99 99 wintypes.LPSTR,
100 100 wintypes.DWORD,
@@ -106,11 +106,11 b' if os.name == "nt":'
106 106 ]
107 107 CreateFile.restype = wintypes.HANDLE
108 108
109 CloseHandle = ctypes.windll.kernel32.CloseHandle
109 CloseHandle = _kernel32.CloseHandle
110 110 CloseHandle.argtypes = [wintypes.HANDLE]
111 111 CloseHandle.restype = wintypes.BOOL
112 112
113 ReadFile = ctypes.windll.kernel32.ReadFile
113 ReadFile = _kernel32.ReadFile
114 114 ReadFile.argtypes = [
115 115 wintypes.HANDLE,
116 116 wintypes.LPVOID,
@@ -120,7 +120,7 b' if os.name == "nt":'
120 120 ]
121 121 ReadFile.restype = wintypes.BOOL
122 122
123 WriteFile = ctypes.windll.kernel32.WriteFile
123 WriteFile = _kernel32.WriteFile
124 124 WriteFile.argtypes = [
125 125 wintypes.HANDLE,
126 126 wintypes.LPVOID,
@@ -130,15 +130,15 b' if os.name == "nt":'
130 130 ]
131 131 WriteFile.restype = wintypes.BOOL
132 132
133 GetLastError = ctypes.windll.kernel32.GetLastError
133 GetLastError = _kernel32.GetLastError
134 134 GetLastError.argtypes = []
135 135 GetLastError.restype = wintypes.DWORD
136 136
137 SetLastError = ctypes.windll.kernel32.SetLastError
137 SetLastError = _kernel32.SetLastError
138 138 SetLastError.argtypes = [wintypes.DWORD]
139 139 SetLastError.restype = None
140 140
141 FormatMessage = ctypes.windll.kernel32.FormatMessageA
141 FormatMessage = _kernel32.FormatMessageA
142 142 FormatMessage.argtypes = [
143 143 wintypes.DWORD,
144 144 wintypes.LPVOID,
@@ -150,9 +150,9 b' if os.name == "nt":'
150 150 ]
151 151 FormatMessage.restype = wintypes.DWORD
152 152
153 LocalFree = ctypes.windll.kernel32.LocalFree
153 LocalFree = _kernel32.LocalFree
154 154
155 GetOverlappedResult = ctypes.windll.kernel32.GetOverlappedResult
155 GetOverlappedResult = _kernel32.GetOverlappedResult
156 156 GetOverlappedResult.argtypes = [
157 157 wintypes.HANDLE,
158 158 ctypes.POINTER(OVERLAPPED),
@@ -161,9 +161,7 b' if os.name == "nt":'
161 161 ]
162 162 GetOverlappedResult.restype = wintypes.BOOL
163 163
164 GetOverlappedResultEx = getattr(
165 ctypes.windll.kernel32, "GetOverlappedResultEx", None
166 )
164 GetOverlappedResultEx = getattr(_kernel32, "GetOverlappedResultEx", None)
167 165 if GetOverlappedResultEx is not None:
168 166 GetOverlappedResultEx.argtypes = [
169 167 wintypes.HANDLE,
@@ -174,7 +172,7 b' if os.name == "nt":'
174 172 ]
175 173 GetOverlappedResultEx.restype = wintypes.BOOL
176 174
177 WaitForSingleObjectEx = ctypes.windll.kernel32.WaitForSingleObjectEx
175 WaitForSingleObjectEx = _kernel32.WaitForSingleObjectEx
178 176 WaitForSingleObjectEx.argtypes = [
179 177 wintypes.HANDLE,
180 178 wintypes.DWORD,
@@ -182,7 +180,7 b' if os.name == "nt":'
182 180 ]
183 181 WaitForSingleObjectEx.restype = wintypes.DWORD
184 182
185 CreateEvent = ctypes.windll.kernel32.CreateEventA
183 CreateEvent = _kernel32.CreateEventA
186 184 CreateEvent.argtypes = [
187 185 LPDWORD,
188 186 wintypes.BOOL,
@@ -192,7 +190,7 b' if os.name == "nt":'
192 190 CreateEvent.restype = wintypes.HANDLE
193 191
194 192 # Windows Vista is the minimum supported client for CancelIoEx.
195 CancelIoEx = ctypes.windll.kernel32.CancelIoEx
193 CancelIoEx = _kernel32.CancelIoEx
196 194 CancelIoEx.argtypes = [wintypes.HANDLE, ctypes.POINTER(OVERLAPPED)]
197 195 CancelIoEx.restype = wintypes.BOOL
198 196
@@ -691,9 +689,9 b' class CLIProcessTransport(Transport):'
691 689 if self.closed:
692 690 self.close()
693 691 self.closed = False
694 self._connect()
695 res = self.proc.stdin.write(data)
696 self.proc.stdin.close()
692 proc = self._connect()
693 res = proc.stdin.write(data)
694 proc.stdin.close()
697 695 self.closed = True
698 696 return res
699 697
@@ -988,8 +986,12 b' class client:'
988 986 # if invoked via an application with graphical user interface,
989 987 # this call will cause a brief command window pop-up.
990 988 # Using the flag STARTF_USESHOWWINDOW to avoid this behavior.
989
990 # pytype: disable=module-attr
991 991 startupinfo = subprocess.STARTUPINFO()
992 992 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
993 # pytype: enable=module-attr
994
993 995 args["startupinfo"] = startupinfo
994 996
995 997 p = subprocess.Popen(cmd, **args)
@@ -1026,7 +1028,11 b' class client:'
1026 1028 if self.transport == CLIProcessTransport:
1027 1029 kwargs["binpath"] = self.binpath
1028 1030
1031 # Only CLIProcessTransport has the binpath kwarg
1032 # pytype: disable=wrong-keyword-args
1029 1033 self.tport = self.transport(self.sockpath, self.timeout, **kwargs)
1034 # pytype: enable=wrong-keyword-args
1035
1030 1036 self.sendConn = self.sendCodec(self.tport)
1031 1037 self.recvConn = self.recvCodec(self.tport)
1032 1038 self.pid = os.getpid()
@@ -26,8 +26,6 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29
32 30 def parse_version(vstr):
33 31 res = 0
@@ -26,45 +26,28 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29 import sys
32 30
33 31
34 32 """Compatibility module across Python 2 and 3."""
35 33
36 34
37 PYTHON2 = sys.version_info < (3, 0)
38 35 PYTHON3 = sys.version_info >= (3, 0)
39 36
40 37 # This is adapted from https://bitbucket.org/gutworth/six, and used under the
41 38 # MIT license. See LICENSE for a full copyright notice.
42 if PYTHON3:
43
44 def reraise(tp, value, tb=None):
45 try:
46 if value is None:
47 value = tp()
48 if value.__traceback__ is not tb:
49 raise value.with_traceback(tb)
50 raise value
51 finally:
52 value = None
53 tb = None
54 39
55 40
56 else:
57 exec(
58 """
59 41 def reraise(tp, value, tb=None):
60 42 try:
61 raise tp, value, tb
43 if value is None:
44 value = tp()
45 if value.__traceback__ is not tb:
46 raise value.with_traceback(tb)
47 raise value
62 48 finally:
49 value = None
63 50 tb = None
64 """.strip()
65 )
51
66 52
67 if PYTHON3:
68 UNICODE = str
69 else:
70 UNICODE = unicode # noqa: F821 We handled versioning above
53 UNICODE = str
@@ -26,8 +26,6 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29 import sys
32 30
33 31 from . import compat
@@ -26,8 +26,6 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29 import ctypes
32 30
33 31
@@ -26,8 +26,6 b''
26 26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 # no unicode literals
30
31 29 import binascii
32 30 import collections
33 31 import ctypes
@@ -53,17 +51,15 b' BSER_TEMPLATE = b"\\x0b"'
53 51 BSER_SKIP = b"\x0c"
54 52 BSER_UTF8STRING = b"\x0d"
55 53
56 if compat.PYTHON3:
57 STRING_TYPES = (str, bytes)
58 unicode = str
54 STRING_TYPES = (str, bytes)
55 unicode = str
56
59 57
60 def tobytes(i):
61 return str(i).encode("ascii")
58 def tobytes(i):
59 return str(i).encode("ascii")
62 60
63 long = int
64 else:
65 STRING_TYPES = (unicode, str)
66 tobytes = bytes
61
62 long = int
67 63
68 64 # Leave room for the serialization header, which includes
69 65 # our overall length. To make things simpler, we'll use an
@@ -89,7 +85,7 b' def _int_size(x):'
89 85 def _buf_pos(buf, pos):
90 86 ret = buf[pos]
91 87 # Normalize the return type to bytes
92 if compat.PYTHON3 and not isinstance(ret, bytes):
88 if not isinstance(ret, bytes):
93 89 ret = bytes((ret,))
94 90 return ret
95 91
@@ -252,10 +248,7 b' class _bser_buffer:'
252 248 else:
253 249 raise RuntimeError("Cannot represent this mapping value")
254 250 self.wpos += needed
255 if compat.PYTHON3:
256 iteritems = val.items()
257 else:
258 iteritems = val.iteritems() # noqa: B301 Checked version above
251 iteritems = val.items()
259 252 for k, v in iteritems:
260 253 self.append_string(k)
261 254 self.append_recursive(v)
@@ -260,7 +260,12 b' class gitdirstate:'
260 260 # # TODO what the heck is this
261 261 _filecache = set()
262 262
263 def pendingparentchange(self):
263 def is_changing_parents(self):
264 # TODO: we need to implement the context manager bits and
265 # correctly stage/revert index edits.
266 return False
267
268 def is_changing_any(self):
264 269 # TODO: we need to implement the context manager bits and
265 270 # correctly stage/revert index edits.
266 271 return False
@@ -322,14 +327,6 b' class gitdirstate:'
322 327 r[path] = s
323 328 return r
324 329
325 def savebackup(self, tr, backupname):
326 # TODO: figure out a strategy for saving index backups.
327 pass
328
329 def restorebackup(self, tr, backupname):
330 # TODO: figure out a strategy for saving index backups.
331 pass
332
333 330 def set_tracked(self, f, reset_copy=False):
334 331 # TODO: support copies and reset_copy=True
335 332 uf = pycompat.fsdecode(f)
@@ -384,7 +381,7 b' class gitdirstate:'
384 381 pass
385 382
386 383 @contextlib.contextmanager
387 def parentchange(self):
384 def changing_parents(self, repo):
388 385 # TODO: track this maybe?
389 386 yield
390 387
@@ -392,10 +389,6 b' class gitdirstate:'
392 389 # TODO: should this be added to the dirstate interface?
393 390 self._plchangecallbacks[category] = callback
394 391
395 def clearbackup(self, tr, backupname):
396 # TODO
397 pass
398
399 392 def setbranch(self, branch):
400 393 raise error.Abort(
401 394 b'git repos do not support branches. try using bookmarks'
@@ -9,7 +9,7 b' def get_pygit2():'
9 9 global pygit2_module
10 10 if pygit2_module is None:
11 11 try:
12 import pygit2 as pygit2_module
12 import pygit2 as pygit2_module # pytype: disable=import-error
13 13
14 14 pygit2_module.InvalidSpecError
15 15 except (ImportError, AttributeError):
@@ -352,7 +352,8 b' def _dosign(ui, repo, *revs, **opts):'
352 352 sigsfile.close()
353 353
354 354 if b'.hgsigs' not in repo.dirstate:
355 repo[None].add([b".hgsigs"])
355 with repo.dirstate.changing_files(repo):
356 repo[None].add([b".hgsigs"])
356 357
357 358 if opts[b"no_commit"]:
358 359 return
@@ -1051,12 +1051,11 b' def findoutgoing(ui, repo, remote=None, '
1051 1051 if opts is None:
1052 1052 opts = {}
1053 1053 path = urlutil.get_unique_push_path(b'histedit', repo, ui, remote)
1054 dest = path.pushloc or path.loc
1055
1056 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(dest))
1054
1055 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(path.loc))
1057 1056
1058 1057 revs, checkout = hg.addbranchrevs(repo, repo, (path.branch, []), None)
1059 other = hg.peer(repo, opts, dest)
1058 other = hg.peer(repo, opts, path)
1060 1059
1061 1060 if revs:
1062 1061 revs = [repo.lookup(rev) for rev in revs]
@@ -32,7 +32,10 b' from mercurial import ('
32 32 pycompat,
33 33 registrar,
34 34 )
35 from mercurial.utils import dateutil
35 from mercurial.utils import (
36 dateutil,
37 stringutil,
38 )
36 39 from .. import notify
37 40
38 41 configtable = {}
@@ -98,7 +101,7 b' def _report_commit(ui, repo, ctx):'
98 101 try:
99 102 msg = mail.parsebytes(data)
100 103 except emailerrors.MessageParseError as inst:
101 raise error.Abort(inst)
104 raise error.Abort(stringutil.forcebytestr(inst))
102 105
103 106 msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
104 107 msg['Message-Id'] = notify.messageid(
@@ -31,7 +31,10 b' from mercurial import ('
31 31 pycompat,
32 32 registrar,
33 33 )
34 from mercurial.utils import dateutil
34 from mercurial.utils import (
35 dateutil,
36 stringutil,
37 )
35 38 from .. import notify
36 39
37 40 configtable = {}
@@ -97,7 +100,7 b' def _report_commit(ui, repo, ctx):'
97 100 try:
98 101 msg = mail.parsebytes(data)
99 102 except emailerrors.MessageParseError as inst:
100 raise error.Abort(inst)
103 raise error.Abort(stringutil.forcebytestr(inst))
101 104
102 105 msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
103 106 msg['Message-Id'] = notify.messageid(
@@ -683,12 +683,10 b' def _lookupwrap(orig):'
683 683 def _pull(orig, ui, repo, source=b"default", **opts):
684 684 opts = pycompat.byteskwargs(opts)
685 685 # Copy paste from `pull` command
686 source, branches = urlutil.get_unique_pull_path(
686 path = urlutil.get_unique_pull_path_obj(
687 687 b"infinite-push's pull",
688 repo,
689 688 ui,
690 689 source,
691 default_branches=opts.get(b'branch'),
692 690 )
693 691
694 692 scratchbookmarks = {}
@@ -709,7 +707,7 b' def _pull(orig, ui, repo, source=b"defau'
709 707 bookmarks.append(bookmark)
710 708
711 709 if scratchbookmarks:
712 other = hg.peer(repo, opts, source)
710 other = hg.peer(repo, opts, path)
713 711 try:
714 712 fetchedbookmarks = other.listkeyspatterns(
715 713 b'bookmarks', patterns=scratchbookmarks
@@ -734,14 +732,14 b' def _pull(orig, ui, repo, source=b"defau'
734 732 try:
735 733 # Remote scratch bookmarks will be deleted because remotenames doesn't
736 734 # know about them. Let's save it before pull and restore after
737 remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, source)
738 result = orig(ui, repo, source, **pycompat.strkwargs(opts))
735 remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, path.loc)
736 result = orig(ui, repo, path.loc, **pycompat.strkwargs(opts))
739 737 # TODO(stash): race condition is possible
740 738 # if scratch bookmarks was updated right after orig.
741 739 # But that's unlikely and shouldn't be harmful.
742 740 if common.isremotebooksenabled(ui):
743 741 remotescratchbookmarks.update(scratchbookmarks)
744 _saveremotebookmarks(repo, remotescratchbookmarks, source)
742 _saveremotebookmarks(repo, remotescratchbookmarks, path.loc)
745 743 else:
746 744 _savelocalbookmarks(repo, scratchbookmarks)
747 745 return result
@@ -849,14 +847,14 b' def _push(orig, ui, repo, *dests, **opts'
849 847 raise error.Abort(msg)
850 848
851 849 path = paths[0]
852 destpath = path.pushloc or path.loc
850 destpath = path.loc
853 851 # Remote scratch bookmarks will be deleted because remotenames doesn't
854 852 # know about them. Let's save it before push and restore after
855 853 remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, destpath)
856 854 result = orig(ui, repo, *dests, **pycompat.strkwargs(opts))
857 855 if common.isremotebooksenabled(ui):
858 856 if bookmark and scratchpush:
859 other = hg.peer(repo, opts, destpath)
857 other = hg.peer(repo, opts, path)
860 858 try:
861 859 fetchedbookmarks = other.listkeyspatterns(
862 860 b'bookmarks', patterns=[bookmark]
@@ -567,8 +567,12 b' def journal(ui, repo, *args, **opts):'
567 567 )
568 568 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
569 569 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
570
571 # ``name`` is bytes, or None only if 'all' was an option.
570 572 fm.condwrite(
573 # pytype: disable=attribute-error
571 574 opts.get(b'all') or name.startswith(b're:'),
575 # pytype: enable=attribute-error
572 576 b'name',
573 577 b' %-8s',
574 578 entry.name,
@@ -437,7 +437,7 b' def _kwfwrite(ui, repo, expand, *pats, *'
437 437 if len(wctx.parents()) > 1:
438 438 raise error.Abort(_(b'outstanding uncommitted merge'))
439 439 kwt = getattr(repo, '_keywordkwt', None)
440 with repo.wlock():
440 with repo.wlock(), repo.dirstate.changing_files(repo):
441 441 status = _status(ui, repo, wctx, kwt, *pats, **opts)
442 442 if status.modified or status.added or status.removed or status.deleted:
443 443 raise error.Abort(_(b'outstanding uncommitted changes'))
@@ -530,17 +530,18 b' def demo(ui, repo, *args, **opts):'
530 530 demoitems(b'keywordmaps', kwmaps.items())
531 531 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
532 532 repo.wvfs.write(fn, keywords)
533 repo[None].add([fn])
534 ui.note(_(b'\nkeywords written to %s:\n') % fn)
535 ui.note(keywords)
536 533 with repo.wlock():
534 with repo.dirstate.changing_files(repo):
535 repo[None].add([fn])
536 ui.note(_(b'\nkeywords written to %s:\n') % fn)
537 ui.note(keywords)
537 538 repo.dirstate.setbranch(b'demobranch')
538 for name, cmd in ui.configitems(b'hooks'):
539 if name.split(b'.', 1)[0].find(b'commit') > -1:
540 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
541 msg = _(b'hg keyword configuration and expansion example')
542 ui.note((b"hg ci -m '%s'\n" % msg))
543 repo.commit(text=msg)
539 for name, cmd in ui.configitems(b'hooks'):
540 if name.split(b'.', 1)[0].find(b'commit') > -1:
541 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
542 msg = _(b'hg keyword configuration and expansion example')
543 ui.note((b"hg ci -m '%s'\n" % msg))
544 repo.commit(text=msg)
544 545 ui.status(_(b'\n\tkeywords expanded\n'))
545 546 ui.write(repo.wread(fn))
546 547 repo.wvfs.rmtree(repo.root)
@@ -696,7 +697,7 b' def kw_amend(orig, ui, repo, old, extra,'
696 697 kwt = getattr(repo, '_keywordkwt', None)
697 698 if kwt is None:
698 699 return orig(ui, repo, old, extra, pats, opts)
699 with repo.wlock(), repo.dirstate.parentchange():
700 with repo.wlock(), repo.dirstate.changing_parents(repo):
700 701 kwt.postcommit = True
701 702 newid = orig(ui, repo, old, extra, pats, opts)
702 703 if newid != old.node():
@@ -762,7 +763,7 b' def kw_dorecord(orig, ui, repo, commitfu'
762 763 if ctx != recctx:
763 764 modified, added = _preselect(wstatus, recctx.files())
764 765 kwt.restrict = False
765 with repo.dirstate.parentchange():
766 with repo.dirstate.changing_parents(repo):
766 767 kwt.overwrite(recctx, modified, False, True)
767 768 kwt.overwrite(recctx, added, False, True, True)
768 769 kwt.restrict = True
@@ -107,6 +107,7 b' command.'
107 107
108 108 from mercurial import (
109 109 cmdutil,
110 configitems,
110 111 extensions,
111 112 exthelper,
112 113 hg,
@@ -135,7 +136,7 b' eh.merge(proto.eh)'
135 136 eh.configitem(
136 137 b'largefiles',
137 138 b'minsize',
138 default=eh.configitem.dynamicdefault,
139 default=configitems.dynamicdefault,
139 140 )
140 141 eh.configitem(
141 142 b'largefiles',
@@ -219,7 +219,9 b' def lfconvert(ui, src, dest, *pats, **op'
219 219 success = True
220 220 finally:
221 221 if tolfile:
222 rdst.dirstate.clear()
222 # XXX is this the right context semantically ?
223 with rdst.dirstate.changing_parents(rdst):
224 rdst.dirstate.clear()
223 225 release(dstlock, dstwlock)
224 226 if not success:
225 227 # we failed, remove the new directory
@@ -517,53 +519,52 b' def updatelfiles('
517 519 filelist = set(filelist)
518 520 lfiles = [f for f in lfiles if f in filelist]
519 521
520 with lfdirstate.parentchange():
521 update = {}
522 dropped = set()
523 updated, removed = 0, 0
524 wvfs = repo.wvfs
525 wctx = repo[None]
526 for lfile in lfiles:
527 lfileorig = os.path.relpath(
528 scmutil.backuppath(ui, repo, lfile), start=repo.root
529 )
530 standin = lfutil.standin(lfile)
531 standinorig = os.path.relpath(
532 scmutil.backuppath(ui, repo, standin), start=repo.root
533 )
534 if wvfs.exists(standin):
535 if wvfs.exists(standinorig) and wvfs.exists(lfile):
536 shutil.copyfile(wvfs.join(lfile), wvfs.join(lfileorig))
537 wvfs.unlinkpath(standinorig)
538 expecthash = lfutil.readasstandin(wctx[standin])
539 if expecthash != b'':
540 if lfile not in wctx: # not switched to normal file
541 if repo.dirstate.get_entry(standin).any_tracked:
542 wvfs.unlinkpath(lfile, ignoremissing=True)
543 else:
544 dropped.add(lfile)
522 update = {}
523 dropped = set()
524 updated, removed = 0, 0
525 wvfs = repo.wvfs
526 wctx = repo[None]
527 for lfile in lfiles:
528 lfileorig = os.path.relpath(
529 scmutil.backuppath(ui, repo, lfile), start=repo.root
530 )
531 standin = lfutil.standin(lfile)
532 standinorig = os.path.relpath(
533 scmutil.backuppath(ui, repo, standin), start=repo.root
534 )
535 if wvfs.exists(standin):
536 if wvfs.exists(standinorig) and wvfs.exists(lfile):
537 shutil.copyfile(wvfs.join(lfile), wvfs.join(lfileorig))
538 wvfs.unlinkpath(standinorig)
539 expecthash = lfutil.readasstandin(wctx[standin])
540 if expecthash != b'':
541 if lfile not in wctx: # not switched to normal file
542 if repo.dirstate.get_entry(standin).any_tracked:
543 wvfs.unlinkpath(lfile, ignoremissing=True)
544 else:
545 dropped.add(lfile)
545 546
546 # use normallookup() to allocate an entry in largefiles
547 # dirstate to prevent lfilesrepo.status() from reporting
548 # missing files as removed.
549 lfdirstate.update_file(
550 lfile,
551 p1_tracked=True,
552 wc_tracked=True,
553 possibly_dirty=True,
554 )
555 update[lfile] = expecthash
556 else:
557 # Remove lfiles for which the standin is deleted, unless the
558 # lfile is added to the repository again. This happens when a
559 # largefile is converted back to a normal file: the standin
560 # disappears, but a new (normal) file appears as the lfile.
561 if (
562 wvfs.exists(lfile)
563 and repo.dirstate.normalize(lfile) not in wctx
564 ):
565 wvfs.unlinkpath(lfile)
566 removed += 1
547 # allocate an entry in largefiles dirstate to prevent
548 # lfilesrepo.status() from reporting missing files as
549 # removed.
550 lfdirstate.hacky_extension_update_file(
551 lfile,
552 p1_tracked=True,
553 wc_tracked=True,
554 possibly_dirty=True,
555 )
556 update[lfile] = expecthash
557 else:
558 # Remove lfiles for which the standin is deleted, unless the
559 # lfile is added to the repository again. This happens when a
560 # largefile is converted back to a normal file: the standin
561 # disappears, but a new (normal) file appears as the lfile.
562 if (
563 wvfs.exists(lfile)
564 and repo.dirstate.normalize(lfile) not in wctx
565 ):
566 wvfs.unlinkpath(lfile)
567 removed += 1
567 568
568 569 # largefile processing might be slow and be interrupted - be prepared
569 570 lfdirstate.write(repo.currenttransaction())
@@ -580,41 +581,42 b' def updatelfiles('
580 581 statuswriter(_(b'getting changed largefiles\n'))
581 582 cachelfiles(ui, repo, None, lfiles)
582 583
583 with lfdirstate.parentchange():
584 for lfile in lfiles:
585 update1 = 0
584 for lfile in lfiles:
585 update1 = 0
586 586
587 expecthash = update.get(lfile)
588 if expecthash:
589 if not lfutil.copyfromcache(repo, expecthash, lfile):
590 # failed ... but already removed and set to normallookup
591 continue
592 # Synchronize largefile dirstate to the last modified
593 # time of the file
594 lfdirstate.update_file(
595 lfile, p1_tracked=True, wc_tracked=True
596 )
587 expecthash = update.get(lfile)
588 if expecthash:
589 if not lfutil.copyfromcache(repo, expecthash, lfile):
590 # failed ... but already removed and set to normallookup
591 continue
592 # Synchronize largefile dirstate to the last modified
593 # time of the file
594 lfdirstate.hacky_extension_update_file(
595 lfile,
596 p1_tracked=True,
597 wc_tracked=True,
598 )
599 update1 = 1
600
601 # copy the exec mode of largefile standin from the repository's
602 # dirstate to its state in the lfdirstate.
603 standin = lfutil.standin(lfile)
604 if wvfs.exists(standin):
605 # exec is decided by the users permissions using mask 0o100
606 standinexec = wvfs.stat(standin).st_mode & 0o100
607 st = wvfs.stat(lfile)
608 mode = st.st_mode
609 if standinexec != mode & 0o100:
610 # first remove all X bits, then shift all R bits to X
611 mode &= ~0o111
612 if standinexec:
613 mode |= (mode >> 2) & 0o111 & ~util.umask
614 wvfs.chmod(lfile, mode)
597 615 update1 = 1
598 616
599 # copy the exec mode of largefile standin from the repository's
600 # dirstate to its state in the lfdirstate.
601 standin = lfutil.standin(lfile)
602 if wvfs.exists(standin):
603 # exec is decided by the users permissions using mask 0o100
604 standinexec = wvfs.stat(standin).st_mode & 0o100
605 st = wvfs.stat(lfile)
606 mode = st.st_mode
607 if standinexec != mode & 0o100:
608 # first remove all X bits, then shift all R bits to X
609 mode &= ~0o111
610 if standinexec:
611 mode |= (mode >> 2) & 0o111 & ~util.umask
612 wvfs.chmod(lfile, mode)
613 update1 = 1
617 updated += update1
614 618
615 updated += update1
616
617 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
619 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
618 620
619 621 lfdirstate.write(repo.currenttransaction())
620 622 if lfiles:
@@ -159,6 +159,9 b' def findfile(repo, hash):'
159 159
160 160
161 161 class largefilesdirstate(dirstate.dirstate):
162 _large_file_dirstate = True
163 _tr_key_suffix = b'-large-files'
164
162 165 def __getitem__(self, key):
163 166 return super(largefilesdirstate, self).__getitem__(unixpath(key))
164 167
@@ -204,7 +207,13 b' def openlfdirstate(ui, repo, create=True'
204 207 """
205 208 Return a dirstate object that tracks largefiles: i.e. its root is
206 209 the repo root, but it is saved in .hg/largefiles/dirstate.
210
211 If a dirstate object already exists and is being used for a 'changing_*'
212 context, it will be returned.
207 213 """
214 sub_dirstate = getattr(repo.dirstate, '_sub_dirstate', None)
215 if sub_dirstate is not None:
216 return sub_dirstate
208 217 vfs = repo.vfs
209 218 lfstoredir = longname
210 219 opener = vfsmod.vfs(vfs.join(lfstoredir))
@@ -223,20 +232,29 b' def openlfdirstate(ui, repo, create=True'
223 232 # it. This ensures that we create it on the first meaningful
224 233 # largefiles operation in a new clone.
225 234 if create and not vfs.exists(vfs.join(lfstoredir, b'dirstate')):
226 matcher = getstandinmatcher(repo)
227 standins = repo.dirstate.walk(
228 matcher, subrepos=[], unknown=False, ignored=False
229 )
235 try:
236 with repo.wlock(wait=False), lfdirstate.changing_files(repo):
237 matcher = getstandinmatcher(repo)
238 standins = repo.dirstate.walk(
239 matcher, subrepos=[], unknown=False, ignored=False
240 )
241
242 if len(standins) > 0:
243 vfs.makedirs(lfstoredir)
230 244
231 if len(standins) > 0:
232 vfs.makedirs(lfstoredir)
233
234 with lfdirstate.parentchange():
235 for standin in standins:
236 lfile = splitstandin(standin)
237 lfdirstate.update_file(
238 lfile, p1_tracked=True, wc_tracked=True, possibly_dirty=True
239 )
245 for standin in standins:
246 lfile = splitstandin(standin)
247 lfdirstate.hacky_extension_update_file(
248 lfile,
249 p1_tracked=True,
250 wc_tracked=True,
251 possibly_dirty=True,
252 )
253 except error.LockError:
254 # Assume that whatever was holding the lock was important.
255 # If we were doing something important, we would already have
256 # either the lock or a largefile dirstate.
257 pass
240 258 return lfdirstate
241 259
242 260
@@ -565,10 +583,14 b' def getstandinsstate(repo):'
565 583 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
566 584 lfstandin = standin(lfile)
567 585 if lfstandin not in repo.dirstate:
568 lfdirstate.update_file(lfile, p1_tracked=False, wc_tracked=False)
586 lfdirstate.hacky_extension_update_file(
587 lfile,
588 p1_tracked=False,
589 wc_tracked=False,
590 )
569 591 else:
570 592 entry = repo.dirstate.get_entry(lfstandin)
571 lfdirstate.update_file(
593 lfdirstate.hacky_extension_update_file(
572 594 lfile,
573 595 wc_tracked=entry.tracked,
574 596 p1_tracked=entry.p1_tracked,
@@ -580,8 +602,7 b' def synclfdirstate(repo, lfdirstate, lfi'
580 602 def markcommitted(orig, ctx, node):
581 603 repo = ctx.repo()
582 604
583 lfdirstate = openlfdirstate(repo.ui, repo)
584 with lfdirstate.parentchange():
605 with repo.dirstate.changing_parents(repo):
585 606 orig(node)
586 607
587 608 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
@@ -593,11 +614,11 b' def markcommitted(orig, ctx, node):'
593 614 # - have to be marked as "n" after commit, but
594 615 # - aren't listed in "repo[node].files()"
595 616
617 lfdirstate = openlfdirstate(repo.ui, repo)
596 618 for f in ctx.files():
597 619 lfile = splitstandin(f)
598 620 if lfile is not None:
599 621 synclfdirstate(repo, lfdirstate, lfile, False)
600 lfdirstate.write(repo.currenttransaction())
601 622
602 623 # As part of committing, copy all of the largefiles into the cache.
603 624 #
@@ -668,11 +689,16 b' def updatestandinsbymatch(repo, match):'
668 689 # It can cost a lot of time (several seconds)
669 690 # otherwise to update all standins if the largefiles are
670 691 # large.
671 lfdirstate = openlfdirstate(ui, repo)
672 692 dirtymatch = matchmod.always()
673 unsure, s, mtime_boundary = lfdirstate.status(
674 dirtymatch, subrepos=[], ignored=False, clean=False, unknown=False
675 )
693 with repo.dirstate.running_status(repo):
694 lfdirstate = openlfdirstate(ui, repo)
695 unsure, s, mtime_boundary = lfdirstate.status(
696 dirtymatch,
697 subrepos=[],
698 ignored=False,
699 clean=False,
700 unknown=False,
701 )
676 702 modifiedfiles = unsure + s.modified + s.added + s.removed
677 703 lfiles = listlfiles(repo)
678 704 # this only loops through largefiles that exist (not
@@ -8,6 +8,7 b''
8 8
9 9 '''Overridden Mercurial commands and functions for the largefiles extension'''
10 10
11 import contextlib
11 12 import copy
12 13 import os
13 14
@@ -21,6 +22,7 b' from mercurial import ('
21 22 archival,
22 23 cmdutil,
23 24 copies as copiesmod,
25 dirstate,
24 26 error,
25 27 exchange,
26 28 extensions,
@@ -311,6 +313,48 b' def cmdutilremove('
311 313 )
312 314
313 315
316 @eh.wrapfunction(dirstate.dirstate, b'_changing')
317 @contextlib.contextmanager
318 def _changing(orig, self, repo, change_type):
319 pre = sub_dirstate = getattr(self, '_sub_dirstate', None)
320 try:
321 lfd = getattr(self, '_large_file_dirstate', False)
322 if sub_dirstate is None and not lfd:
323 sub_dirstate = lfutil.openlfdirstate(repo.ui, repo)
324 self._sub_dirstate = sub_dirstate
325 if not lfd:
326 assert self._sub_dirstate is not None
327 with orig(self, repo, change_type):
328 if sub_dirstate is None:
329 yield
330 else:
331 with sub_dirstate._changing(repo, change_type):
332 yield
333 finally:
334 self._sub_dirstate = pre
335
336
337 @eh.wrapfunction(dirstate.dirstate, b'running_status')
338 @contextlib.contextmanager
339 def running_status(orig, self, repo):
340 pre = sub_dirstate = getattr(self, '_sub_dirstate', None)
341 try:
342 lfd = getattr(self, '_large_file_dirstate', False)
343 if sub_dirstate is None and not lfd:
344 sub_dirstate = lfutil.openlfdirstate(repo.ui, repo)
345 self._sub_dirstate = sub_dirstate
346 if not lfd:
347 assert self._sub_dirstate is not None
348 with orig(self, repo):
349 if sub_dirstate is None:
350 yield
351 else:
352 with sub_dirstate.running_status(repo):
353 yield
354 finally:
355 self._sub_dirstate = pre
356
357
314 358 @eh.wrapfunction(subrepo.hgsubrepo, b'status')
315 359 def overridestatusfn(orig, repo, rev2, **opts):
316 360 with lfstatus(repo._repo):
@@ -511,10 +555,12 b' def overridedebugstate(orig, ui, repo, *'
511 555 # largefiles. This makes the merge proceed and we can then handle this
512 556 # case further in the overridden calculateupdates function below.
513 557 @eh.wrapfunction(merge, b'_checkunknownfile')
514 def overridecheckunknownfile(origfn, repo, wctx, mctx, f, f2=None):
515 if lfutil.standin(repo.dirstate.normalize(f)) in wctx:
558 def overridecheckunknownfile(
559 origfn, dirstate, wvfs, dircache, wctx, mctx, f, f2=None
560 ):
561 if lfutil.standin(dirstate.normalize(f)) in wctx:
516 562 return False
517 return origfn(repo, wctx, mctx, f, f2)
563 return origfn(dirstate, wvfs, dircache, wctx, mctx, f, f2)
518 564
519 565
520 566 # The manifest merge handles conflicts on the manifest level. We want
@@ -658,18 +704,12 b' def overridecalculateupdates('
658 704 def mergerecordupdates(orig, repo, actions, branchmerge, getfiledata):
659 705 if MERGE_ACTION_LARGEFILE_MARK_REMOVED in actions:
660 706 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
661 with lfdirstate.parentchange():
662 for lfile, args, msg in actions[
663 MERGE_ACTION_LARGEFILE_MARK_REMOVED
664 ]:
665 # this should be executed before 'orig', to execute 'remove'
666 # before all other actions
667 repo.dirstate.update_file(
668 lfile, p1_tracked=True, wc_tracked=False
669 )
670 # make sure lfile doesn't get synclfdirstate'd as normal
671 lfdirstate.update_file(lfile, p1_tracked=False, wc_tracked=True)
672 lfdirstate.write(repo.currenttransaction())
707 for lfile, args, msg in actions[MERGE_ACTION_LARGEFILE_MARK_REMOVED]:
708 # this should be executed before 'orig', to execute 'remove'
709 # before all other actions
710 repo.dirstate.update_file(lfile, p1_tracked=True, wc_tracked=False)
711 # make sure lfile doesn't get synclfdirstate'd as normal
712 lfdirstate.update_file(lfile, p1_tracked=False, wc_tracked=True)
673 713
674 714 return orig(repo, actions, branchmerge, getfiledata)
675 715
@@ -901,7 +941,7 b' def overriderevert(orig, ui, repo, ctx, '
901 941 # Because we put the standins in a bad state (by updating them)
902 942 # and then return them to a correct state we need to lock to
903 943 # prevent others from changing them in their incorrect state.
904 with repo.wlock():
944 with repo.wlock(), repo.dirstate.running_status(repo):
905 945 lfdirstate = lfutil.openlfdirstate(ui, repo)
906 946 s = lfutil.lfdirstatestatus(lfdirstate, repo)
907 947 lfdirstate.write(repo.currenttransaction())
@@ -1436,7 +1476,7 b' def outgoinghook(ui, repo, other, opts, '
1436 1476
1437 1477 def addfunc(fn, lfhash):
1438 1478 if fn not in toupload:
1439 toupload[fn] = []
1479 toupload[fn] = [] # pytype: disable=unsupported-operands
1440 1480 toupload[fn].append(lfhash)
1441 1481 lfhashes.add(lfhash)
1442 1482
@@ -1520,20 +1560,34 b' def overridesummary(orig, ui, repo, *pat'
1520 1560
1521 1561
1522 1562 @eh.wrapfunction(scmutil, b'addremove')
1523 def scmutiladdremove(orig, repo, matcher, prefix, uipathfn, opts=None):
1563 def scmutiladdremove(
1564 orig,
1565 repo,
1566 matcher,
1567 prefix,
1568 uipathfn,
1569 opts=None,
1570 open_tr=None,
1571 ):
1524 1572 if opts is None:
1525 1573 opts = {}
1526 1574 if not lfutil.islfilesrepo(repo):
1527 return orig(repo, matcher, prefix, uipathfn, opts)
1575 return orig(repo, matcher, prefix, uipathfn, opts, open_tr=open_tr)
1576
1577 # open the transaction and changing_files context
1578 if open_tr is not None:
1579 open_tr()
1580
1528 1581 # Get the list of missing largefiles so we can remove them
1529 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1530 unsure, s, mtime_boundary = lfdirstate.status(
1531 matchmod.always(),
1532 subrepos=[],
1533 ignored=False,
1534 clean=False,
1535 unknown=False,
1536 )
1582 with repo.dirstate.running_status(repo):
1583 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1584 unsure, s, mtime_boundary = lfdirstate.status(
1585 matchmod.always(),
1586 subrepos=[],
1587 ignored=False,
1588 clean=False,
1589 unknown=False,
1590 )
1537 1591
1538 1592 # Call into the normal remove code, but the removing of the standin, we want
1539 1593 # to have handled by original addremove. Monkey patching here makes sure
@@ -1567,7 +1621,8 b' def scmutiladdremove(orig, repo, matcher'
1567 1621 # function to take care of the rest. Make sure it doesn't do anything with
1568 1622 # largefiles by passing a matcher that will ignore them.
1569 1623 matcher = composenormalfilematcher(matcher, repo[None].manifest(), added)
1570 return orig(repo, matcher, prefix, uipathfn, opts)
1624
1625 return orig(repo, matcher, prefix, uipathfn, opts, open_tr=open_tr)
1571 1626
1572 1627
1573 1628 # Calling purge with --all will cause the largefiles to be deleted.
@@ -1737,7 +1792,7 b' def mergeupdate(orig, repo, node, branch'
1737 1792 matcher = kwargs.get('matcher', None)
1738 1793 # note if this is a partial update
1739 1794 partial = matcher and not matcher.always()
1740 with repo.wlock():
1795 with repo.wlock(), repo.dirstate.changing_parents(repo):
1741 1796 # branch | | |
1742 1797 # merge | force | partial | action
1743 1798 # -------+-------+---------+--------------
@@ -1752,15 +1807,15 b' def mergeupdate(orig, repo, node, branch'
1752 1807 #
1753 1808 # (*) don't care
1754 1809 # (*1) deprecated, but used internally (e.g: "rebase --collapse")
1755
1756 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1757 unsure, s, mtime_boundary = lfdirstate.status(
1758 matchmod.always(),
1759 subrepos=[],
1760 ignored=False,
1761 clean=True,
1762 unknown=False,
1763 )
1810 with repo.dirstate.running_status(repo):
1811 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1812 unsure, s, mtime_boundary = lfdirstate.status(
1813 matchmod.always(),
1814 subrepos=[],
1815 ignored=False,
1816 clean=True,
1817 unknown=False,
1818 )
1764 1819 oldclean = set(s.clean)
1765 1820 pctx = repo[b'.']
1766 1821 dctx = repo[node]
@@ -1787,7 +1842,14 b' def mergeupdate(orig, repo, node, branch'
1787 1842 # mark all clean largefiles as dirty, just in case the update gets
1788 1843 # interrupted before largefiles and lfdirstate are synchronized
1789 1844 for lfile in oldclean:
1790 lfdirstate.set_possibly_dirty(lfile)
1845 entry = lfdirstate.get_entry(lfile)
1846 lfdirstate.hacky_extension_update_file(
1847 lfile,
1848 wc_tracked=entry.tracked,
1849 p1_tracked=entry.p1_tracked,
1850 p2_info=entry.p2_info,
1851 possibly_dirty=True,
1852 )
1791 1853 lfdirstate.write(repo.currenttransaction())
1792 1854
1793 1855 oldstandins = lfutil.getstandinsstate(repo)
@@ -1798,24 +1860,22 b' def mergeupdate(orig, repo, node, branch'
1798 1860 raise error.ProgrammingError(
1799 1861 b'largefiles is not compatible with in-memory merge'
1800 1862 )
1801 with lfdirstate.parentchange():
1802 result = orig(repo, node, branchmerge, force, *args, **kwargs)
1863 result = orig(repo, node, branchmerge, force, *args, **kwargs)
1803 1864
1804 newstandins = lfutil.getstandinsstate(repo)
1805 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
1865 newstandins = lfutil.getstandinsstate(repo)
1866 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
1806 1867
1807 # to avoid leaving all largefiles as dirty and thus rehash them, mark
1808 # all the ones that didn't change as clean
1809 for lfile in oldclean.difference(filelist):
1810 lfdirstate.update_file(lfile, p1_tracked=True, wc_tracked=True)
1811 lfdirstate.write(repo.currenttransaction())
1868 # to avoid leaving all largefiles as dirty and thus rehash them, mark
1869 # all the ones that didn't change as clean
1870 for lfile in oldclean.difference(filelist):
1871 lfdirstate.update_file(lfile, p1_tracked=True, wc_tracked=True)
1812 1872
1813 if branchmerge or force or partial:
1814 filelist.extend(s.deleted + s.removed)
1873 if branchmerge or force or partial:
1874 filelist.extend(s.deleted + s.removed)
1815 1875
1816 lfcommands.updatelfiles(
1817 repo.ui, repo, filelist=filelist, normallookup=partial
1818 )
1876 lfcommands.updatelfiles(
1877 repo.ui, repo, filelist=filelist, normallookup=partial
1878 )
1819 1879
1820 1880 return result
1821 1881
@@ -139,7 +139,7 b' def reposetup(ui, repo):'
139 139 except error.LockError:
140 140 wlock = util.nullcontextmanager()
141 141 gotlock = False
142 with wlock:
142 with wlock, self.dirstate.running_status(self):
143 143
144 144 # First check if paths or patterns were specified on the
145 145 # command line. If there were, and they don't match any
@@ -321,6 +321,8 b' def reposetup(ui, repo):'
321 321
322 322 if gotlock:
323 323 lfdirstate.write(self.currenttransaction())
324 else:
325 lfdirstate.invalidate()
324 326
325 327 self.lfstatus = True
326 328 return scmutil.status(*result)
@@ -36,22 +36,23 b' def openstore(repo=None, remote=None, pu'
36 36 b'lfpullsource', repo, ui, lfpullsource
37 37 )
38 38 else:
39 path, _branches = urlutil.get_unique_pull_path(
40 b'lfpullsource', repo, ui, lfpullsource
39 path = urlutil.get_unique_pull_path_obj(
40 b'lfpullsource', ui, lfpullsource
41 41 )
42 42
43 43 # XXX we should not explicitly pass b'default', as this will result in
44 44 # b'default' being returned if no `paths.default` was defined. We
45 45 # should explicitely handle the lack of value instead.
46 46 if repo is None:
47 path, _branches = urlutil.get_unique_pull_path(
48 b'lfs', repo, ui, b'default'
47 path = urlutil.get_unique_pull_path_obj(
48 b'lfs',
49 ui,
50 b'default',
49 51 )
50 52 remote = hg.peer(repo or ui, {}, path)
51 elif path == b'default-push' or path == b'default':
53 elif path.loc == b'default-push' or path.loc == b'default':
52 54 remote = repo
53 55 else:
54 path, _branches = urlutil.parseurl(path)
55 56 remote = hg.peer(repo or ui, {}, path)
56 57
57 58 # The path could be a scheme so use Mercurial's normal functionality
@@ -168,12 +168,16 b' class local:'
168 168 # producing the response (but the server has no way of telling us
169 169 # that), and we really don't need to try to write the response to
170 170 # the localstore, because it's not going to match the expected.
171 # The server also uses this method to store data uploaded by the
172 # client, so if this happens on the server side, it's possible
173 # that the client crashed or an antivirus interfered with the
174 # upload.
171 175 if content_length is not None and int(content_length) != size:
172 176 msg = (
173 177 b"Response length (%d) does not match Content-Length "
174 b"header (%d): likely server-side crash"
178 b"header (%d) for %s"
175 179 )
176 raise LfsRemoteError(_(msg) % (size, int(content_length)))
180 raise LfsRemoteError(_(msg) % (size, int(content_length), oid))
177 181
178 182 realoid = hex(sha256.digest())
179 183 if realoid != oid:
@@ -82,7 +82,6 b' from mercurial.pycompat import ('
82 82 from mercurial import (
83 83 cmdutil,
84 84 commands,
85 dirstateguard,
86 85 encoding,
87 86 error,
88 87 extensions,
@@ -791,7 +790,10 b' class queue:'
791 790 if self.added:
792 791 qrepo = self.qrepo()
793 792 if qrepo:
794 qrepo[None].add(f for f in self.added if f not in qrepo[None])
793 with qrepo.wlock(), qrepo.dirstate.changing_files(qrepo):
794 qrepo[None].add(
795 f for f in self.added if f not in qrepo[None]
796 )
795 797 self.added = []
796 798
797 799 def removeundo(self, repo):
@@ -1082,7 +1084,7 b' class queue:'
1082 1084
1083 1085 if merge and files:
1084 1086 # Mark as removed/merged and update dirstate parent info
1085 with repo.dirstate.parentchange():
1087 with repo.dirstate.changing_parents(repo):
1086 1088 for f in files:
1087 1089 repo.dirstate.update_file_p1(f, p1_tracked=True)
1088 1090 p1 = repo.dirstate.p1()
@@ -1129,7 +1131,8 b' class queue:'
1129 1131 if not keep:
1130 1132 r = self.qrepo()
1131 1133 if r:
1132 r[None].forget(patches)
1134 with r.wlock(), r.dirstate.changing_files(r):
1135 r[None].forget(patches)
1133 1136 for p in patches:
1134 1137 try:
1135 1138 os.unlink(self.join(p))
@@ -1153,7 +1156,7 b' class queue:'
1153 1156 sortedseries.append((idx, p))
1154 1157
1155 1158 sortedseries.sort(reverse=True)
1156 for (i, p) in sortedseries:
1159 for i, p in sortedseries:
1157 1160 if i != -1:
1158 1161 del self.fullseries[i]
1159 1162 else:
@@ -1177,7 +1180,6 b' class queue:'
1177 1180 firstrev = repo[self.applied[0].node].rev()
1178 1181 patches = []
1179 1182 for i, rev in enumerate(revs):
1180
1181 1183 if rev < firstrev:
1182 1184 raise error.Abort(_(b'revision %d is not managed') % rev)
1183 1185
@@ -1465,7 +1467,8 b' class queue:'
1465 1467 p.close()
1466 1468 r = self.qrepo()
1467 1469 if r:
1468 r[None].add([patchfn])
1470 with r.wlock(), r.dirstate.changing_files(r):
1471 r[None].add([patchfn])
1469 1472 except: # re-raises
1470 1473 repo.rollback()
1471 1474 raise
@@ -1830,7 +1833,7 b' class queue:'
1830 1833 if keepchanges and tobackup:
1831 1834 raise error.Abort(_(b"local changes found, qrefresh first"))
1832 1835 self.backup(repo, tobackup)
1833 with repo.dirstate.parentchange():
1836 with repo.dirstate.changing_parents(repo):
1834 1837 for f in a:
1835 1838 repo.wvfs.unlinkpath(f, ignoremissing=True)
1836 1839 repo.dirstate.update_file(
@@ -1988,73 +1991,67 b' class queue:'
1988 1991
1989 1992 bmlist = repo[top].bookmarks()
1990 1993
1991 with repo.dirstate.parentchange():
1992 # XXX do we actually need the dirstateguard
1993 dsguard = None
1994 try:
1995 dsguard = dirstateguard.dirstateguard(repo, b'mq.refresh')
1996 if diffopts.git or diffopts.upgrade:
1997 copies = {}
1998 for dst in a:
1999 src = repo.dirstate.copied(dst)
2000 # during qfold, the source file for copies may
2001 # be removed. Treat this as a simple add.
2002 if src is not None and src in repo.dirstate:
2003 copies.setdefault(src, []).append(dst)
2004 repo.dirstate.update_file(
2005 dst, p1_tracked=False, wc_tracked=True
1994 with repo.dirstate.changing_parents(repo):
1995 if diffopts.git or diffopts.upgrade:
1996 copies = {}
1997 for dst in a:
1998 src = repo.dirstate.copied(dst)
1999 # during qfold, the source file for copies may
2000 # be removed. Treat this as a simple add.
2001 if src is not None and src in repo.dirstate:
2002 copies.setdefault(src, []).append(dst)
2003 repo.dirstate.update_file(
2004 dst, p1_tracked=False, wc_tracked=True
2005 )
2006 # remember the copies between patchparent and qtip
2007 for dst in aaa:
2008 src = ctx[dst].copysource()
2009 if src:
2010 copies.setdefault(src, []).extend(
2011 copies.get(dst, [])
2006 2012 )
2007 # remember the copies between patchparent and qtip
2008 for dst in aaa:
2009 src = ctx[dst].copysource()
2010 if src:
2011 copies.setdefault(src, []).extend(
2012 copies.get(dst, [])
2013 )
2014 if dst in a:
2015 copies[src].append(dst)
2016 # we can't copy a file created by the patch itself
2017 if dst in copies:
2018 del copies[dst]
2019 for src, dsts in copies.items():
2020 for dst in dsts:
2021 repo.dirstate.copy(src, dst)
2022 else:
2023 for dst in a:
2024 repo.dirstate.update_file(
2025 dst, p1_tracked=False, wc_tracked=True
2026 )
2027 # Drop useless copy information
2028 for f in list(repo.dirstate.copies()):
2029 repo.dirstate.copy(None, f)
2030 for f in r:
2031 repo.dirstate.update_file_p1(f, p1_tracked=True)
2032 # if the patch excludes a modified file, mark that
2033 # file with mtime=0 so status can see it.
2034 mm = []
2035 for i in range(len(m) - 1, -1, -1):
2036 if not match1(m[i]):
2037 mm.append(m[i])
2038 del m[i]
2039 for f in m:
2040 repo.dirstate.update_file_p1(f, p1_tracked=True)
2041 for f in mm:
2042 repo.dirstate.update_file_p1(f, p1_tracked=True)
2043 for f in forget:
2044 repo.dirstate.update_file_p1(f, p1_tracked=False)
2045
2046 user = ph.user or ctx.user()
2047
2048 oldphase = repo[top].phase()
2049
2050 # assumes strip can roll itself back if interrupted
2051 repo.setparents(*cparents)
2052 self.applied.pop()
2053 self.applieddirty = True
2054 strip(self.ui, repo, [top], update=False, backup=False)
2055 dsguard.close()
2056 finally:
2057 release(dsguard)
2013 if dst in a:
2014 copies[src].append(dst)
2015 # we can't copy a file created by the patch itself
2016 if dst in copies:
2017 del copies[dst]
2018 for src, dsts in copies.items():
2019 for dst in dsts:
2020 repo.dirstate.copy(src, dst)
2021 else:
2022 for dst in a:
2023 repo.dirstate.update_file(
2024 dst, p1_tracked=False, wc_tracked=True
2025 )
2026 # Drop useless copy information
2027 for f in list(repo.dirstate.copies()):
2028 repo.dirstate.copy(None, f)
2029 for f in r:
2030 repo.dirstate.update_file_p1(f, p1_tracked=True)
2031 # if the patch excludes a modified file, mark that
2032 # file with mtime=0 so status can see it.
2033 mm = []
2034 for i in range(len(m) - 1, -1, -1):
2035 if not match1(m[i]):
2036 mm.append(m[i])
2037 del m[i]
2038 for f in m:
2039 repo.dirstate.update_file_p1(f, p1_tracked=True)
2040 for f in mm:
2041 repo.dirstate.update_file_p1(f, p1_tracked=True)
2042 for f in forget:
2043 repo.dirstate.update_file_p1(f, p1_tracked=False)
2044
2045 user = ph.user or ctx.user()
2046
2047 oldphase = repo[top].phase()
2048
2049 # assumes strip can roll itself back if interrupted
2050 repo.setparents(*cparents)
2051 repo.dirstate.write(repo.currenttransaction())
2052 self.applied.pop()
2053 self.applieddirty = True
2054 strip(self.ui, repo, [top], update=False, backup=False)
2058 2055
2059 2056 try:
2060 2057 # might be nice to attempt to roll back strip after this
@@ -2124,8 +2121,9 b' class queue:'
2124 2121 finally:
2125 2122 lockmod.release(tr, lock)
2126 2123 except: # re-raises
2127 ctx = repo[cparents[0]]
2128 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
2124 with repo.dirstate.changing_parents(repo):
2125 ctx = repo[cparents[0]]
2126 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
2129 2127 self.savedirty()
2130 2128 self.ui.warn(
2131 2129 _(
@@ -2760,18 +2758,19 b' def qinit(ui, repo, create):'
2760 2758 r = q.init(repo, create)
2761 2759 q.savedirty()
2762 2760 if r:
2763 if not os.path.exists(r.wjoin(b'.hgignore')):
2764 fp = r.wvfs(b'.hgignore', b'w')
2765 fp.write(b'^\\.hg\n')
2766 fp.write(b'^\\.mq\n')
2767 fp.write(b'syntax: glob\n')
2768 fp.write(b'status\n')
2769 fp.write(b'guards\n')
2770 fp.close()
2771 if not os.path.exists(r.wjoin(b'series')):
2772 r.wvfs(b'series', b'w').close()
2773 r[None].add([b'.hgignore', b'series'])
2774 commands.add(ui, r)
2761 with r.wlock(), r.dirstate.changing_files(r):
2762 if not os.path.exists(r.wjoin(b'.hgignore')):
2763 fp = r.wvfs(b'.hgignore', b'w')
2764 fp.write(b'^\\.hg\n')
2765 fp.write(b'^\\.mq\n')
2766 fp.write(b'syntax: glob\n')
2767 fp.write(b'status\n')
2768 fp.write(b'guards\n')
2769 fp.close()
2770 if not os.path.exists(r.wjoin(b'series')):
2771 r.wvfs(b'series', b'w').close()
2772 r[None].add([b'.hgignore', b'series'])
2773 commands.add(ui, r)
2775 2774 return 0
2776 2775
2777 2776
@@ -2854,16 +2853,17 b' def clone(ui, source, dest=None, **opts)'
2854 2853 # main repo (destination and sources)
2855 2854 if dest is None:
2856 2855 dest = hg.defaultdest(source)
2857 __, source_path, __ = urlutil.get_clone_path(ui, source)
2856 source_path = urlutil.get_clone_path_obj(ui, source)
2858 2857 sr = hg.peer(ui, opts, source_path)
2859 2858
2860 2859 # patches repo (source only)
2861 2860 if opts.get(b'patches'):
2862 __, patchespath, __ = urlutil.get_clone_path(ui, opts.get(b'patches'))
2861 patches_path = urlutil.get_clone_path_obj(ui, opts.get(b'patches'))
2863 2862 else:
2864 patchespath = patchdir(sr)
2863 # XXX path: we should turn this into a path object
2864 patches_path = patchdir(sr)
2865 2865 try:
2866 hg.peer(ui, opts, patchespath)
2866 hg.peer(ui, opts, patches_path)
2867 2867 except error.RepoError:
2868 2868 raise error.Abort(
2869 2869 _(b'versioned patch repository not found (see init --mq)')
@@ -3223,45 +3223,46 b' def fold(ui, repo, *files, **opts):'
3223 3223 raise error.Abort(_(b'qfold requires at least one patch name'))
3224 3224 if not q.checktoppatch(repo)[0]:
3225 3225 raise error.Abort(_(b'no patches applied'))
3226 q.checklocalchanges(repo)
3227
3228 message = cmdutil.logmessage(ui, opts)
3229
3230 parent = q.lookup(b'qtip')
3231 patches = []
3232 messages = []
3233 for f in files:
3234 p = q.lookup(f)
3235 if p in patches or p == parent:
3236 ui.warn(_(b'skipping already folded patch %s\n') % p)
3237 if q.isapplied(p):
3238 raise error.Abort(
3239 _(b'qfold cannot fold already applied patch %s') % p
3240 )
3241 patches.append(p)
3242
3243 for p in patches:
3226
3227 with repo.wlock():
3228 q.checklocalchanges(repo)
3229
3230 message = cmdutil.logmessage(ui, opts)
3231
3232 parent = q.lookup(b'qtip')
3233 patches = []
3234 messages = []
3235 for f in files:
3236 p = q.lookup(f)
3237 if p in patches or p == parent:
3238 ui.warn(_(b'skipping already folded patch %s\n') % p)
3239 if q.isapplied(p):
3240 raise error.Abort(
3241 _(b'qfold cannot fold already applied patch %s') % p
3242 )
3243 patches.append(p)
3244
3245 for p in patches:
3246 if not message:
3247 ph = patchheader(q.join(p), q.plainmode)
3248 if ph.message:
3249 messages.append(ph.message)
3250 pf = q.join(p)
3251 (patchsuccess, files, fuzz) = q.patch(repo, pf)
3252 if not patchsuccess:
3253 raise error.Abort(_(b'error folding patch %s') % p)
3254
3244 3255 if not message:
3245 ph = patchheader(q.join(p), q.plainmode)
3246 if ph.message:
3247 messages.append(ph.message)
3248 pf = q.join(p)
3249 (patchsuccess, files, fuzz) = q.patch(repo, pf)
3250 if not patchsuccess:
3251 raise error.Abort(_(b'error folding patch %s') % p)
3252
3253 if not message:
3254 ph = patchheader(q.join(parent), q.plainmode)
3255 message = ph.message
3256 for msg in messages:
3257 if msg:
3258 if message:
3259 message.append(b'* * *')
3260 message.extend(msg)
3261 message = b'\n'.join(message)
3262
3263 diffopts = q.patchopts(q.diffopts(), *patches)
3264 with repo.wlock():
3256 ph = patchheader(q.join(parent), q.plainmode)
3257 message = ph.message
3258 for msg in messages:
3259 if msg:
3260 if message:
3261 message.append(b'* * *')
3262 message.extend(msg)
3263 message = b'\n'.join(message)
3264
3265 diffopts = q.patchopts(q.diffopts(), *patches)
3265 3266 q.refresh(
3266 3267 repo,
3267 3268 msg=message,
@@ -3627,8 +3628,8 b' def rename(ui, repo, patch, name=None, *'
3627 3628 util.rename(q.join(patch), absdest)
3628 3629 r = q.qrepo()
3629 3630 if r and patch in r.dirstate:
3630 wctx = r[None]
3631 with r.wlock():
3631 with r.wlock(), r.dirstate.changing_files(r):
3632 wctx = r[None]
3632 3633 if r.dirstate.get_entry(patch).added:
3633 3634 r.dirstate.set_untracked(patch)
3634 3635 r.dirstate.set_tracked(name)
@@ -320,7 +320,7 b' def _narrow('
320 320 repo.store.markremoved(f)
321 321
322 322 ui.status(_(b'deleting unwanted files from working copy\n'))
323 with repo.dirstate.parentchange():
323 with repo.dirstate.changing_parents(repo):
324 324 narrowspec.updateworkingcopy(repo, assumeclean=True)
325 325 narrowspec.copytoworkingcopy(repo)
326 326
@@ -380,7 +380,7 b' def _widen('
380 380 if ellipsesremote:
381 381 ds = repo.dirstate
382 382 p1, p2 = ds.p1(), ds.p2()
383 with ds.parentchange():
383 with ds.changing_parents(repo):
384 384 ds.setparents(repo.nullid, repo.nullid)
385 385 if isoldellipses:
386 386 with wrappedextraprepare:
@@ -416,13 +416,15 b' def _widen('
416 416 repo, trmanager.transaction, source=b'widen'
417 417 )
418 418 # TODO: we should catch error.Abort here
419 bundle2.processbundle(repo, bundle, op=op)
419 bundle2.processbundle(repo, bundle, op=op, remote=remote)
420 420
421 421 if ellipsesremote:
422 with ds.parentchange():
422 with ds.changing_parents(repo):
423 423 ds.setparents(p1, p2)
424 424
425 with repo.transaction(b'widening'), repo.dirstate.parentchange():
425 with repo.transaction(b'widening'), repo.dirstate.changing_parents(
426 repo
427 ):
426 428 repo.setnewnarrowpats()
427 429 narrowspec.updateworkingcopy(repo)
428 430 narrowspec.copytoworkingcopy(repo)
@@ -591,7 +593,7 b' def trackedcmd(ui, repo, remotepath=None'
591 593 if update_working_copy:
592 594 with repo.wlock(), repo.lock(), repo.transaction(
593 595 b'narrow-wc'
594 ), repo.dirstate.parentchange():
596 ), repo.dirstate.changing_parents(repo):
595 597 narrowspec.updateworkingcopy(repo)
596 598 narrowspec.copytoworkingcopy(repo)
597 599 return 0
@@ -606,10 +608,9 b' def trackedcmd(ui, repo, remotepath=None'
606 608 # Find the revisions we have in common with the remote. These will
607 609 # be used for finding local-only changes for narrowing. They will
608 610 # also define the set of revisions to update for widening.
609 r = urlutil.get_unique_pull_path(b'tracked', repo, ui, remotepath)
610 url, branches = r
611 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(url))
612 remote = hg.peer(repo, opts, url)
611 path = urlutil.get_unique_pull_path_obj(b'tracked', ui, remotepath)
612 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(path.loc))
613 remote = hg.peer(repo, opts, path)
613 614
614 615 try:
615 616 # check narrow support before doing anything if widening needs to be
@@ -19,8 +19,8 b' def wraprepo(repo):'
19 19 dirstate = super(narrowrepository, self)._makedirstate()
20 20 return narrowdirstate.wrapdirstate(self, dirstate)
21 21
22 def peer(self):
23 peer = super(narrowrepository, self).peer()
22 def peer(self, path=None):
23 peer = super(narrowrepository, self).peer(path=path)
24 24 peer._caps.add(wireprototypes.NARROWCAP)
25 25 peer._caps.add(wireprototypes.ELLIPSESCAP)
26 26 return peer
@@ -450,7 +450,7 b' class notifier:'
450 450 try:
451 451 msg = mail.parsebytes(data)
452 452 except emailerrors.MessageParseError as inst:
453 raise error.Abort(inst)
453 raise error.Abort(stringutil.forcebytestr(inst))
454 454
455 455 # store sender and subject
456 456 sender = msg['From']
@@ -286,9 +286,12 b' def vcrcommand(name, flags, spec, helpca'
286 286 import hgdemandimport
287 287
288 288 with hgdemandimport.deactivated():
289 # pytype: disable=import-error
289 290 import vcr as vcrmod
290 291 import vcr.stubs as stubs
291 292
293 # pytype: enable=import-error
294
292 295 vcr = vcrmod.VCR(
293 296 serializer='json',
294 297 before_record_request=sanitiserequest,
@@ -350,11 +353,14 b' def urlencodenested(params):'
350 353 """
351 354 flatparams = util.sortdict()
352 355
353 def process(prefix, obj):
356 def process(prefix: bytes, obj):
354 357 if isinstance(obj, bool):
355 358 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
356 359 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
360 # .items() will only be called for a dict type
361 # pytype: disable=attribute-error
357 362 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
363 # pytype: enable=attribute-error
358 364 if items is None:
359 365 flatparams[prefix] = obj
360 366 else:
@@ -30,7 +30,6 b' from mercurial import ('
30 30 commands,
31 31 copies,
32 32 destutil,
33 dirstateguard,
34 33 error,
35 34 extensions,
36 35 logcmdutil,
@@ -1271,15 +1270,9 b' def _origrebase(ui, repo, action, opts, '
1271 1270 # one transaction here. Otherwise, transactions are obtained when
1272 1271 # committing each node, which is slower but allows partial success.
1273 1272 with util.acceptintervention(tr):
1274 # Same logic for the dirstate guard, except we don't create one when
1275 # rebasing in-memory (it's not needed).
1276 dsguard = None
1277 if singletr and not rbsrt.inmemory:
1278 dsguard = dirstateguard.dirstateguard(repo, b'rebase')
1279 with util.acceptintervention(dsguard):
1280 rbsrt._performrebase(tr)
1281 if not rbsrt.dryrun:
1282 rbsrt._finishrebase()
1273 rbsrt._performrebase(tr)
1274 if not rbsrt.dryrun:
1275 rbsrt._finishrebase()
1283 1276
1284 1277
1285 1278 def _definedestmap(ui, repo, inmemory, destf, srcf, basef, revf, destspace):
@@ -1500,10 +1493,10 b' def commitmemorynode(repo, wctx, editor,'
1500 1493 def commitnode(repo, editor, extra, user, date, commitmsg):
1501 1494 """Commit the wd changes with parents p1 and p2.
1502 1495 Return node of committed revision."""
1503 dsguard = util.nullcontextmanager()
1496 tr = util.nullcontextmanager
1504 1497 if not repo.ui.configbool(b'rebase', b'singletransaction'):
1505 dsguard = dirstateguard.dirstateguard(repo, b'rebase')
1506 with dsguard:
1498 tr = lambda: repo.transaction(b'rebase')
1499 with tr():
1507 1500 # Commit might fail if unresolved files exist
1508 1501 newnode = repo.commit(
1509 1502 text=commitmsg, user=user, date=date, extra=extra, editor=editor
@@ -1520,12 +1513,14 b' def rebasenode(repo, rev, p1, p2, base, '
1520 1513 p1ctx = repo[p1]
1521 1514 if wctx.isinmemory():
1522 1515 wctx.setbase(p1ctx)
1516 scope = util.nullcontextmanager
1523 1517 else:
1524 1518 if repo[b'.'].rev() != p1:
1525 1519 repo.ui.debug(b" update to %d:%s\n" % (p1, p1ctx))
1526 1520 mergemod.clean_update(p1ctx)
1527 1521 else:
1528 1522 repo.ui.debug(b" already in destination\n")
1523 scope = lambda: repo.dirstate.changing_parents(repo)
1529 1524 # This is, alas, necessary to invalidate workingctx's manifest cache,
1530 1525 # as well as other data we litter on it in other places.
1531 1526 wctx = repo[None]
@@ -1535,26 +1530,27 b' def rebasenode(repo, rev, p1, p2, base, '
1535 1530 if base is not None:
1536 1531 repo.ui.debug(b" detach base %d:%s\n" % (base, repo[base]))
1537 1532
1538 # See explanation in merge.graft()
1539 mergeancestor = repo.changelog.isancestor(p1ctx.node(), ctx.node())
1540 stats = mergemod._update(
1541 repo,
1542 rev,
1543 branchmerge=True,
1544 force=True,
1545 ancestor=base,
1546 mergeancestor=mergeancestor,
1547 labels=[b'dest', b'source', b'parent of source'],
1548 wc=wctx,
1549 )
1550 wctx.setparents(p1ctx.node(), repo[p2].node())
1551 if collapse:
1552 copies.graftcopies(wctx, ctx, p1ctx)
1553 else:
1554 # If we're not using --collapse, we need to
1555 # duplicate copies between the revision we're
1556 # rebasing and its first parent.
1557 copies.graftcopies(wctx, ctx, ctx.p1())
1533 with scope():
1534 # See explanation in merge.graft()
1535 mergeancestor = repo.changelog.isancestor(p1ctx.node(), ctx.node())
1536 stats = mergemod._update(
1537 repo,
1538 rev,
1539 branchmerge=True,
1540 force=True,
1541 ancestor=base,
1542 mergeancestor=mergeancestor,
1543 labels=[b'dest', b'source', b'parent of source'],
1544 wc=wctx,
1545 )
1546 wctx.setparents(p1ctx.node(), repo[p2].node())
1547 if collapse:
1548 copies.graftcopies(wctx, ctx, p1ctx)
1549 else:
1550 # If we're not using --collapse, we need to
1551 # duplicate copies between the revision we're
1552 # rebasing and its first parent.
1553 copies.graftcopies(wctx, ctx, ctx.p1())
1558 1554
1559 1555 if stats.unresolvedcount > 0:
1560 1556 if wctx.isinmemory():
@@ -39,7 +39,7 b' command = registrar.command(cmdtable)'
39 39 try:
40 40 # Silence a warning about python-Levenshtein.
41 41 #
42 # We don't need the the performance that much and it get anoying in tests.
42 # We don't need the performance that much and it gets annoying in tests.
43 43 import warnings
44 44
45 45 with warnings.catch_warnings():
@@ -50,7 +50,7 b' try:'
50 50 module="fuzzywuzzy.fuzz",
51 51 )
52 52
53 import fuzzywuzzy.fuzz as fuzz
53 import fuzzywuzzy.fuzz as fuzz # pytype: disable=import-error
54 54
55 55 fuzz.token_set_ratio
56 56 except ImportError:
@@ -67,8 +67,8 b' def relink(ui, repo, origin=None, **opts'
67 67
68 68 if origin is None and b'default-relink' in ui.paths:
69 69 origin = b'default-relink'
70 path, __ = urlutil.get_unique_pull_path(b'relink', repo, ui, origin)
71 src = hg.repository(repo.baseui, path)
70 path = urlutil.get_unique_pull_path_obj(b'relink', ui, origin)
71 src = hg.repository(repo.baseui, path.loc)
72 72 ui.status(_(b'relinking %s to %s\n') % (src.store.path, repo.store.path))
73 73 if repo.root == src.root:
74 74 ui.status(_(b'there is nothing to relink\n'))
@@ -299,6 +299,7 b' class remotefilelog:'
299 299 deltaprevious=False,
300 300 deltamode=None,
301 301 sidedata_helpers=None,
302 debug_info=None,
302 303 ):
303 304 # we don't use any of these parameters here
304 305 del nodesorder, revisiondata, assumehaveparentrevisions, deltaprevious
@@ -247,7 +247,7 b' def parsesizeflags(raw):'
247 247 index = raw.index(b'\0')
248 248 except ValueError:
249 249 raise BadRemotefilelogHeader(
250 "unexpected remotefilelog header: illegal format"
250 b"unexpected remotefilelog header: illegal format"
251 251 )
252 252 header = raw[:index]
253 253 if header.startswith(b'v'):
@@ -267,7 +267,7 b' def parsesizeflags(raw):'
267 267 size = int(header)
268 268 if size is None:
269 269 raise BadRemotefilelogHeader(
270 "unexpected remotefilelog header: no size found"
270 b"unexpected remotefilelog header: no size found"
271 271 )
272 272 return index + 1, size, flags
273 273
@@ -80,9 +80,25 b' class ShortRepository:'
80 80 def __repr__(self):
81 81 return b'<ShortRepository: %s>' % self.scheme
82 82
83 def make_peer(self, ui, path, *args, **kwargs):
84 new_url = self.resolve(path.rawloc)
85 path = path.copy(new_raw_location=new_url)
86 cls = hg.peer_schemes.get(path.url.scheme)
87 if cls is not None:
88 return cls.make_peer(ui, path, *args, **kwargs)
89 return None
90
83 91 def instance(self, ui, url, create, intents=None, createopts=None):
84 92 url = self.resolve(url)
85 return hg._peerlookup(url).instance(
93 u = urlutil.url(url)
94 scheme = u.scheme or b'file'
95 if scheme in hg.peer_schemes:
96 cls = hg.peer_schemes[scheme]
97 elif scheme in hg.repo_schemes:
98 cls = hg.repo_schemes[scheme]
99 else:
100 cls = hg.LocalFactory
101 return cls.instance(
86 102 ui, url, create, intents=intents, createopts=createopts
87 103 )
88 104
@@ -119,24 +135,29 b' schemes = {'
119 135 }
120 136
121 137
138 def _check_drive_letter(scheme: bytes) -> None:
139 """check if a scheme conflict with a Windows drive letter"""
140 if (
141 pycompat.iswindows
142 and len(scheme) == 1
143 and scheme.isalpha()
144 and os.path.exists(b'%s:\\' % scheme)
145 ):
146 msg = _(b'custom scheme %s:// conflicts with drive letter %s:\\\n')
147 msg %= (scheme, scheme.upper())
148 raise error.Abort(msg)
149
150
122 151 def extsetup(ui):
123 152 schemes.update(dict(ui.configitems(b'schemes')))
124 153 t = templater.engine(templater.parse)
125 154 for scheme, url in schemes.items():
126 if (
127 pycompat.iswindows
128 and len(scheme) == 1
129 and scheme.isalpha()
130 and os.path.exists(b'%s:\\' % scheme)
131 ):
132 raise error.Abort(
133 _(
134 b'custom scheme %s:// conflicts with drive '
135 b'letter %s:\\\n'
136 )
137 % (scheme, scheme.upper())
138 )
139 hg.schemes[scheme] = ShortRepository(url, scheme, t)
155 _check_drive_letter(scheme)
156 url_scheme = urlutil.url(url).scheme
157 if url_scheme in hg.peer_schemes:
158 hg.peer_schemes[scheme] = ShortRepository(url, scheme, t)
159 else:
160 hg.repo_schemes[scheme] = ShortRepository(url, scheme, t)
140 161
141 162 extensions.wrapfunction(urlutil, b'hasdriveletter', hasdriveletter)
142 163
@@ -144,7 +165,11 b' def extsetup(ui):'
144 165 @command(b'debugexpandscheme', norepo=True)
145 166 def expandscheme(ui, url, **opts):
146 167 """given a repo path, provide the scheme-expanded path"""
147 repo = hg._peerlookup(url)
148 if isinstance(repo, ShortRepository):
149 url = repo.resolve(url)
168 scheme = urlutil.url(url).scheme
169 if scheme in hg.peer_schemes:
170 cls = hg.peer_schemes[scheme]
171 else:
172 cls = hg.repo_schemes.get(scheme)
173 if cls is not None and isinstance(cls, ShortRepository):
174 url = cls.resolve(url)
150 175 ui.write(url + b'\n')
@@ -134,7 +134,7 b' def dosplit(ui, repo, tr, ctx, opts):'
134 134 # Set working parent to ctx.p1(), and keep working copy as ctx's content
135 135 if ctx.node() != repo.dirstate.p1():
136 136 hg.clean(repo, ctx.node(), show_stats=False)
137 with repo.dirstate.parentchange():
137 with repo.dirstate.changing_parents(repo):
138 138 scmutil.movedirstate(repo, ctx.p1())
139 139
140 140 # Any modified, added, removed, deleted result means split is incomplete
@@ -80,7 +80,7 b' from mercurial.utils import ('
80 80 )
81 81
82 82 try:
83 from mercurial import zstd
83 from mercurial import zstd # pytype: disable=import-error
84 84
85 85 zstd.__version__
86 86 except ImportError:
@@ -608,6 +608,7 b' class sqlitefilestore:'
608 608 assumehaveparentrevisions=False,
609 609 deltamode=repository.CG_DELTAMODE_STD,
610 610 sidedata_helpers=None,
611 debug_info=None,
611 612 ):
612 613 if nodesorder not in (b'nodes', b'storage', b'linear', None):
613 614 raise error.ProgrammingError(
@@ -817,8 +817,8 b' def _dotransplant(ui, repo, *revs, **opt'
817 817
818 818 sourcerepo = opts.get(b'source')
819 819 if sourcerepo:
820 u = urlutil.get_unique_pull_path(b'transplant', repo, ui, sourcerepo)[0]
821 peer = hg.peer(repo, opts, u)
820 path = urlutil.get_unique_pull_path_obj(b'transplant', ui, sourcerepo)
821 peer = hg.peer(repo, opts, path)
822 822 heads = pycompat.maplist(peer.lookup, opts.get(b'branch', ()))
823 823 target = set(heads)
824 824 for r in revs:
@@ -236,7 +236,7 b' def uncommit(ui, repo, *pats, **opts):'
236 236 # Fully removed the old commit
237 237 mapping[old.node()] = ()
238 238
239 with repo.dirstate.parentchange():
239 with repo.dirstate.changing_parents(repo):
240 240 scmutil.movedirstate(repo, repo[newid], match)
241 241
242 242 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
@@ -317,7 +317,7 b' def unamend(ui, repo, **opts):'
317 317 newpredctx = repo[newprednode]
318 318 dirstate = repo.dirstate
319 319
320 with dirstate.parentchange():
320 with dirstate.changing_parents(repo):
321 321 scmutil.movedirstate(repo, newpredctx)
322 322
323 323 mapping = {curctx.node(): (newprednode,)}
@@ -216,17 +216,23 b' def reposetup(ui, repo):'
216 216 def wrap_revert(orig, repo, ctx, names, uipathfn, actions, *args, **kwargs):
217 217 # reset dirstate cache for file we touch
218 218 ds = repo.dirstate
219 with ds.parentchange():
220 for filename in actions[b'revert'][0]:
221 entry = ds.get_entry(filename)
222 if entry is not None:
223 if entry.p1_tracked:
224 ds.update_file(
225 filename,
226 entry.tracked,
227 p1_tracked=True,
228 p2_info=entry.p2_info,
229 )
219 for filename in actions[b'revert'][0]:
220 entry = ds.get_entry(filename)
221 if entry is not None:
222 if entry.p1_tracked:
223 # If we revert the file, it is possibly dirty. However,
224 # this extension meddle with the file content and therefore
225 # its size. As a result, we cannot simply call
226 # `dirstate.set_possibly_dirty` as it will not affet the
227 # expected size of the file.
228 #
229 # At least, now, the quirk is properly documented.
230 ds.hacky_extension_update_file(
231 filename,
232 entry.tracked,
233 p1_tracked=entry.p1_tracked,
234 p2_info=entry.p2_info,
235 )
230 236 return orig(repo, ctx, names, uipathfn, actions, *args, **kwargs)
231 237
232 238
@@ -154,9 +154,14 b' class tarit:'
154 154 )
155 155 self.fileobj = gzfileobj
156 156 return (
157 # taropen() wants Literal['a', 'r', 'w', 'x'] for the mode,
158 # but Literal[] is only available in 3.8+ without the
159 # typing_extensions backport.
160 # pytype: disable=wrong-arg-types
157 161 tarfile.TarFile.taropen( # pytype: disable=attribute-error
158 162 name, pycompat.sysstr(mode), gzfileobj
159 163 )
164 # pytype: enable=wrong-arg-types
160 165 )
161 166 else:
162 167 try:
@@ -315,8 +315,17 b' class bundleoperation:'
315 315 * a way to construct a bundle response when applicable.
316 316 """
317 317
318 def __init__(self, repo, transactiongetter, captureoutput=True, source=b''):
318 def __init__(
319 self,
320 repo,
321 transactiongetter,
322 captureoutput=True,
323 source=b'',
324 remote=None,
325 ):
319 326 self.repo = repo
327 # the peer object who produced this bundle if available
328 self.remote = remote
320 329 self.ui = repo.ui
321 330 self.records = unbundlerecords()
322 331 self.reply = None
@@ -363,7 +372,7 b' def _notransaction():'
363 372 raise TransactionUnavailable()
364 373
365 374
366 def applybundle(repo, unbundler, tr, source, url=None, **kwargs):
375 def applybundle(repo, unbundler, tr, source, url=None, remote=None, **kwargs):
367 376 # transform me into unbundler.apply() as soon as the freeze is lifted
368 377 if isinstance(unbundler, unbundle20):
369 378 tr.hookargs[b'bundle2'] = b'1'
@@ -371,10 +380,12 b' def applybundle(repo, unbundler, tr, sou'
371 380 tr.hookargs[b'source'] = source
372 381 if url is not None and b'url' not in tr.hookargs:
373 382 tr.hookargs[b'url'] = url
374 return processbundle(repo, unbundler, lambda: tr, source=source)
383 return processbundle(
384 repo, unbundler, lambda: tr, source=source, remote=remote
385 )
375 386 else:
376 387 # the transactiongetter won't be used, but we might as well set it
377 op = bundleoperation(repo, lambda: tr, source=source)
388 op = bundleoperation(repo, lambda: tr, source=source, remote=remote)
378 389 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
379 390 return op
380 391
@@ -450,7 +461,14 b' class partiterator:'
450 461 )
451 462
452 463
453 def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''):
464 def processbundle(
465 repo,
466 unbundler,
467 transactiongetter=None,
468 op=None,
469 source=b'',
470 remote=None,
471 ):
454 472 """This function process a bundle, apply effect to/from a repo
455 473
456 474 It iterates over each part then searches for and uses the proper handling
@@ -466,7 +484,12 b' def processbundle(repo, unbundler, trans'
466 484 if op is None:
467 485 if transactiongetter is None:
468 486 transactiongetter = _notransaction
469 op = bundleoperation(repo, transactiongetter, source=source)
487 op = bundleoperation(
488 repo,
489 transactiongetter,
490 source=source,
491 remote=remote,
492 )
470 493 # todo:
471 494 # - replace this is a init function soon.
472 495 # - exception catching
@@ -494,6 +517,10 b' def processparts(repo, op, unbundler):'
494 517
495 518
496 519 def _processchangegroup(op, cg, tr, source, url, **kwargs):
520 if op.remote is not None and op.remote.path is not None:
521 remote_path = op.remote.path
522 kwargs = kwargs.copy()
523 kwargs['delta_base_reuse_policy'] = remote_path.delta_reuse_policy
497 524 ret = cg.apply(op.repo, tr, source, url, **kwargs)
498 525 op.records.add(
499 526 b'changegroup',
@@ -1938,7 +1965,12 b' def writebundle('
1938 1965 raise error.Abort(
1939 1966 _(b'old bundle types only supports v1 changegroups')
1940 1967 )
1968
1969 # HG20 is the case without 2 values to unpack, but is handled above.
1970 # pytype: disable=bad-unpacking
1941 1971 header, comp = bundletypes[bundletype]
1972 # pytype: enable=bad-unpacking
1973
1942 1974 if comp not in util.compengines.supportedbundletypes:
1943 1975 raise error.Abort(_(b'unknown stream compression type: %s') % comp)
1944 1976 compengine = util.compengines.forbundletype(comp)
@@ -5,6 +5,10 b''
5 5
6 6 import collections
7 7
8 from typing import (
9 cast,
10 )
11
8 12 from .i18n import _
9 13
10 14 from .thirdparty import attr
@@ -247,7 +251,7 b' def parsebundlespec(repo, spec, strict=T'
247 251 # required to apply it. If we see this metadata, compare against what the
248 252 # repo supports and error if the bundle isn't compatible.
249 253 if version == b'packed1' and b'requirements' in params:
250 requirements = set(params[b'requirements'].split(b','))
254 requirements = set(cast(bytes, params[b'requirements']).split(b','))
251 255 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
252 256 if missingreqs:
253 257 raise error.UnsupportedBundleSpecification(
@@ -88,7 +88,7 b' class bundlerevlog(revlog.revlog):'
88 88 )
89 89
90 90 if not self.index.has_node(deltabase):
91 raise LookupError(
91 raise error.LookupError(
92 92 deltabase, self.display_id, _(b'unknown delta base')
93 93 )
94 94
@@ -458,8 +458,8 b' class bundlerepository:'
458 458 def cancopy(self):
459 459 return False
460 460
461 def peer(self):
462 return bundlepeer(self)
461 def peer(self, path=None):
462 return bundlepeer(self, path=path)
463 463
464 464 def getcwd(self):
465 465 return encoding.getcwd() # always outside the repo
@@ -5,7 +5,7 b' from typing import ('
5 5
6 6 version: int
7 7
8 def bdiff(a: bytes, b: bytes): bytes
8 def bdiff(a: bytes, b: bytes) -> bytes: ...
9 9 def blocks(a: bytes, b: bytes) -> List[Tuple[int, int, int, int]]: ...
10 10 def fixws(s: bytes, allws: bool) -> bytes: ...
11 11 def splitnewlines(text: bytes) -> List[bytes]: ...
@@ -2,6 +2,7 b' from typing import ('
2 2 AnyStr,
3 3 IO,
4 4 List,
5 Optional,
5 6 Sequence,
6 7 )
7 8
@@ -15,7 +16,7 b' class stat:'
15 16 st_mtime: int
16 17 st_ctime: int
17 18
18 def listdir(path: bytes, st: bool, skip: bool) -> List[stat]: ...
19 def listdir(path: bytes, st: bool, skip: Optional[bool]) -> List[stat]: ...
19 20 def posixfile(name: AnyStr, mode: bytes, buffering: int) -> IO: ...
20 21 def statfiles(names: Sequence[bytes]) -> List[stat]: ...
21 22 def setprocname(name: bytes) -> None: ...
@@ -177,7 +177,7 b' static inline bool dirstate_item_c_remov'
177 177 (dirstate_flag_p1_tracked | dirstate_flag_p2_info));
178 178 }
179 179
180 static inline bool dirstate_item_c_merged(dirstateItemObject *self)
180 static inline bool dirstate_item_c_modified(dirstateItemObject *self)
181 181 {
182 182 return ((self->flags & dirstate_flag_wc_tracked) &&
183 183 (self->flags & dirstate_flag_p1_tracked) &&
@@ -195,7 +195,7 b' static inline char dirstate_item_c_v1_st'
195 195 {
196 196 if (dirstate_item_c_removed(self)) {
197 197 return 'r';
198 } else if (dirstate_item_c_merged(self)) {
198 } else if (dirstate_item_c_modified(self)) {
199 199 return 'm';
200 200 } else if (dirstate_item_c_added(self)) {
201 201 return 'a';
@@ -642,9 +642,9 b' static PyObject *dirstate_item_get_p2_in'
642 642 }
643 643 };
644 644
645 static PyObject *dirstate_item_get_merged(dirstateItemObject *self)
645 static PyObject *dirstate_item_get_modified(dirstateItemObject *self)
646 646 {
647 if (dirstate_item_c_merged(self)) {
647 if (dirstate_item_c_modified(self)) {
648 648 Py_RETURN_TRUE;
649 649 } else {
650 650 Py_RETURN_FALSE;
@@ -709,7 +709,7 b' static PyGetSetDef dirstate_item_getset['
709 709 NULL},
710 710 {"added", (getter)dirstate_item_get_added, NULL, "added", NULL},
711 711 {"p2_info", (getter)dirstate_item_get_p2_info, NULL, "p2_info", NULL},
712 {"merged", (getter)dirstate_item_get_merged, NULL, "merged", NULL},
712 {"modified", (getter)dirstate_item_get_modified, NULL, "modified", NULL},
713 713 {"from_p2", (getter)dirstate_item_get_from_p2, NULL, "from_p2", NULL},
714 714 {"maybe_clean", (getter)dirstate_item_get_maybe_clean, NULL, "maybe_clean",
715 715 NULL},
@@ -1187,7 +1187,7 b' void dirs_module_init(PyObject *mod);'
1187 1187 void manifest_module_init(PyObject *mod);
1188 1188 void revlog_module_init(PyObject *mod);
1189 1189
1190 static const int version = 20;
1190 static const int version = 21;
1191 1191
1192 1192 static void module_init(PyObject *mod)
1193 1193 {
@@ -76,3 +76,7 b' class nodetree:'
76 76
77 77 def insert(self, rev: int) -> None: ...
78 78 def shortest(self, node: bytes) -> int: ...
79
80 # The IndexObject type here is defined in C, and there's no type for a buffer
81 # return, as of py3.11. https://github.com/python/typing/issues/593
82 def parse_index2(data: object, inline: object, format: int = ...) -> Tuple[object, Optional[Tuple[int, object]]]: ...
@@ -1446,16 +1446,25 b' static PyObject *index_issnapshot(indexO'
1446 1446 static PyObject *index_findsnapshots(indexObject *self, PyObject *args)
1447 1447 {
1448 1448 Py_ssize_t start_rev;
1449 Py_ssize_t end_rev;
1449 1450 PyObject *cache;
1450 1451 Py_ssize_t base;
1451 1452 Py_ssize_t rev;
1452 1453 PyObject *key = NULL;
1453 1454 PyObject *value = NULL;
1454 1455 const Py_ssize_t length = index_length(self);
1455 if (!PyArg_ParseTuple(args, "O!n", &PyDict_Type, &cache, &start_rev)) {
1456 if (!PyArg_ParseTuple(args, "O!nn", &PyDict_Type, &cache, &start_rev,
1457 &end_rev)) {
1456 1458 return NULL;
1457 1459 }
1458 for (rev = start_rev; rev < length; rev++) {
1460 end_rev += 1;
1461 if (end_rev > length) {
1462 end_rev = length;
1463 }
1464 if (start_rev < 0) {
1465 start_rev = 0;
1466 }
1467 for (rev = start_rev; rev < end_rev; rev++) {
1459 1468 int issnap;
1460 1469 PyObject *allvalues = NULL;
1461 1470 issnap = index_issnapshotrev(self, rev);
@@ -1480,7 +1489,7 b' static PyObject *index_findsnapshots(ind'
1480 1489 }
1481 1490 if (allvalues == NULL) {
1482 1491 int r;
1483 allvalues = PyList_New(0);
1492 allvalues = PySet_New(0);
1484 1493 if (!allvalues) {
1485 1494 goto bail;
1486 1495 }
@@ -1491,7 +1500,7 b' static PyObject *index_findsnapshots(ind'
1491 1500 }
1492 1501 }
1493 1502 value = PyLong_FromSsize_t(rev);
1494 if (PyList_Append(allvalues, value)) {
1503 if (PySet_Add(allvalues, value)) {
1495 1504 goto bail;
1496 1505 }
1497 1506 Py_CLEAR(key);
@@ -8,6 +8,11 b''
8 8
9 9 import struct
10 10
11 from typing import (
12 List,
13 Tuple,
14 )
15
11 16 from ..pure.bdiff import *
12 17 from . import _bdiff # pytype: disable=import-error
13 18
@@ -15,7 +20,7 b' ffi = _bdiff.ffi'
15 20 lib = _bdiff.lib
16 21
17 22
18 def blocks(sa, sb):
23 def blocks(sa: bytes, sb: bytes) -> List[Tuple[int, int, int, int]]:
19 24 a = ffi.new(b"struct bdiff_line**")
20 25 b = ffi.new(b"struct bdiff_line**")
21 26 ac = ffi.new(b"char[]", str(sa))
@@ -29,7 +34,7 b' def blocks(sa, sb):'
29 34 count = lib.bdiff_diff(a[0], an, b[0], bn, l)
30 35 if count < 0:
31 36 raise MemoryError
32 rl = [None] * count
37 rl = [(0, 0, 0, 0)] * count
33 38 h = l.next
34 39 i = 0
35 40 while h:
@@ -43,7 +48,7 b' def blocks(sa, sb):'
43 48 return rl
44 49
45 50
46 def bdiff(sa, sb):
51 def bdiff(sa: bytes, sb: bytes) -> bytes:
47 52 a = ffi.new(b"struct bdiff_line**")
48 53 b = ffi.new(b"struct bdiff_line**")
49 54 ac = ffi.new(b"char[]", str(sa))
@@ -6,6 +6,8 b''
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 from typing import List
10
9 11 from ..pure.mpatch import *
10 12 from ..pure.mpatch import mpatchError # silence pyflakes
11 13 from . import _mpatch # pytype: disable=import-error
@@ -26,7 +28,7 b' def cffi_get_next_item(arg, pos):'
26 28 return container[0]
27 29
28 30
29 def patches(text, bins):
31 def patches(text: bytes, bins: List[bytes]) -> bytes:
30 32 lgt = len(bins)
31 33 all = []
32 34 if not lgt:
@@ -105,6 +105,164 b' def writechunks(ui, chunks, filename, vf'
105 105 os.unlink(cleanup)
106 106
107 107
108 def _dbg_ubdl_line(
109 ui,
110 indent,
111 key,
112 base_value=None,
113 percentage_base=None,
114 percentage_key=None,
115 ):
116 """Print one line of debug_unbundle_debug_info"""
117 line = b"DEBUG-UNBUNDLING: "
118 line += b' ' * (2 * indent)
119 key += b":"
120 padding = b''
121 if base_value is not None:
122 assert len(key) + 1 + (2 * indent) <= _KEY_PART_WIDTH
123 line += key.ljust(_KEY_PART_WIDTH - (2 * indent))
124 if isinstance(base_value, float):
125 line += b"%14.3f seconds" % base_value
126 else:
127 line += b"%10d" % base_value
128 padding = b' '
129 else:
130 line += key
131
132 if percentage_base is not None:
133 line += padding
134 padding = b''
135 assert base_value is not None
136 percentage = base_value * 100 // percentage_base
137 if percentage_key is not None:
138 line += b" (%3d%% of %s)" % (
139 percentage,
140 percentage_key,
141 )
142 else:
143 line += b" (%3d%%)" % percentage
144
145 line += b'\n'
146 ui.write_err(line)
147
148
149 def _sumf(items):
150 # python < 3.8 does not support a `start=0.0` argument to sum
151 # So we have to cheat a bit until we drop support for those version
152 if not items:
153 return 0.0
154 return sum(items)
155
156
157 def display_unbundle_debug_info(ui, debug_info):
158 """display an unbundling report from debug information"""
159 cl_info = []
160 mn_info = []
161 fl_info = []
162 _dispatch = [
163 (b'CHANGELOG:', cl_info),
164 (b'MANIFESTLOG:', mn_info),
165 (b'FILELOG:', fl_info),
166 ]
167 for e in debug_info:
168 for prefix, info in _dispatch:
169 if e["target-revlog"].startswith(prefix):
170 info.append(e)
171 break
172 else:
173 assert False, 'unreachable'
174 each_info = [
175 (b'changelog', cl_info),
176 (b'manifests', mn_info),
177 (b'files', fl_info),
178 ]
179
180 # General Revision Countss
181 _dbg_ubdl_line(ui, 0, b'revisions', len(debug_info))
182 for key, info in each_info:
183 if not info:
184 continue
185 _dbg_ubdl_line(ui, 1, key, len(info), len(debug_info))
186
187 # General Time spent
188 all_durations = [e['duration'] for e in debug_info]
189 all_durations.sort()
190 total_duration = _sumf(all_durations)
191 _dbg_ubdl_line(ui, 0, b'total-time', total_duration)
192
193 for key, info in each_info:
194 if not info:
195 continue
196 durations = [e['duration'] for e in info]
197 durations.sort()
198 _dbg_ubdl_line(ui, 1, key, _sumf(durations), total_duration)
199
200 # Count and cache reuse per delta types
201 each_types = {}
202 for key, info in each_info:
203 each_types[key] = types = {
204 b'full': 0,
205 b'full-cached': 0,
206 b'snapshot': 0,
207 b'snapshot-cached': 0,
208 b'delta': 0,
209 b'delta-cached': 0,
210 b'unknown': 0,
211 b'unknown-cached': 0,
212 }
213 for e in info:
214 types[e['type']] += 1
215 if e['using-cached-base']:
216 types[e['type'] + b'-cached'] += 1
217
218 EXPECTED_TYPES = (b'full', b'snapshot', b'delta', b'unknown')
219 if debug_info:
220 _dbg_ubdl_line(ui, 0, b'type-count')
221 for key, info in each_info:
222 if info:
223 _dbg_ubdl_line(ui, 1, key)
224 t = each_types[key]
225 for tn in EXPECTED_TYPES:
226 if t[tn]:
227 tc = tn + b'-cached'
228 _dbg_ubdl_line(ui, 2, tn, t[tn])
229 _dbg_ubdl_line(ui, 3, b'cached', t[tc], t[tn])
230
231 # time perf delta types and reuse
232 each_type_time = {}
233 for key, info in each_info:
234 each_type_time[key] = t = {
235 b'full': [],
236 b'full-cached': [],
237 b'snapshot': [],
238 b'snapshot-cached': [],
239 b'delta': [],
240 b'delta-cached': [],
241 b'unknown': [],
242 b'unknown-cached': [],
243 }
244 for e in info:
245 t[e['type']].append(e['duration'])
246 if e['using-cached-base']:
247 t[e['type'] + b'-cached'].append(e['duration'])
248 for t_key, value in list(t.items()):
249 value.sort()
250 t[t_key] = _sumf(value)
251
252 if debug_info:
253 _dbg_ubdl_line(ui, 0, b'type-time')
254 for key, info in each_info:
255 if info:
256 _dbg_ubdl_line(ui, 1, key)
257 t = each_type_time[key]
258 td = total_duration # to same space on next lines
259 for tn in EXPECTED_TYPES:
260 if t[tn]:
261 tc = tn + b'-cached'
262 _dbg_ubdl_line(ui, 2, tn, t[tn], td, b"total")
263 _dbg_ubdl_line(ui, 3, b'cached', t[tc], td, b"total")
264
265
108 266 class cg1unpacker:
109 267 """Unpacker for cg1 changegroup streams.
110 268
@@ -254,7 +412,16 b' class cg1unpacker:'
254 412 pos = next
255 413 yield closechunk()
256 414
257 def _unpackmanifests(self, repo, revmap, trp, prog, addrevisioncb=None):
415 def _unpackmanifests(
416 self,
417 repo,
418 revmap,
419 trp,
420 prog,
421 addrevisioncb=None,
422 debug_info=None,
423 delta_base_reuse_policy=None,
424 ):
258 425 self.callback = prog.increment
259 426 # no need to check for empty manifest group here:
260 427 # if the result of the merge of 1 and 2 is the same in 3 and 4,
@@ -263,7 +430,14 b' class cg1unpacker:'
263 430 self.manifestheader()
264 431 deltas = self.deltaiter()
265 432 storage = repo.manifestlog.getstorage(b'')
266 storage.addgroup(deltas, revmap, trp, addrevisioncb=addrevisioncb)
433 storage.addgroup(
434 deltas,
435 revmap,
436 trp,
437 addrevisioncb=addrevisioncb,
438 debug_info=debug_info,
439 delta_base_reuse_policy=delta_base_reuse_policy,
440 )
267 441 prog.complete()
268 442 self.callback = None
269 443
@@ -276,6 +450,7 b' class cg1unpacker:'
276 450 targetphase=phases.draft,
277 451 expectedtotal=None,
278 452 sidedata_categories=None,
453 delta_base_reuse_policy=None,
279 454 ):
280 455 """Add the changegroup returned by source.read() to this repo.
281 456 srctype is a string like 'push', 'pull', or 'unbundle'. url is
@@ -289,9 +464,19 b' class cg1unpacker:'
289 464
290 465 `sidedata_categories` is an optional set of the remote's sidedata wanted
291 466 categories.
467
468 `delta_base_reuse_policy` is an optional argument, when set to a value
469 it will control the way the delta contained into the bundle are reused
470 when applied in the revlog.
471
472 See `DELTA_BASE_REUSE_*` entry in mercurial.revlogutils.constants.
292 473 """
293 474 repo = repo.unfiltered()
294 475
476 debug_info = None
477 if repo.ui.configbool(b'debug', b'unbundling-stats'):
478 debug_info = []
479
295 480 # Only useful if we're adding sidedata categories. If both peers have
296 481 # the same categories, then we simply don't do anything.
297 482 adding_sidedata = (
@@ -366,6 +551,8 b' class cg1unpacker:'
366 551 alwayscache=True,
367 552 addrevisioncb=onchangelog,
368 553 duplicaterevisioncb=ondupchangelog,
554 debug_info=debug_info,
555 delta_base_reuse_policy=delta_base_reuse_policy,
369 556 ):
370 557 repo.ui.develwarn(
371 558 b'applied empty changelog from changegroup',
@@ -413,6 +600,8 b' class cg1unpacker:'
413 600 trp,
414 601 progress,
415 602 addrevisioncb=on_manifest_rev,
603 debug_info=debug_info,
604 delta_base_reuse_policy=delta_base_reuse_policy,
416 605 )
417 606
418 607 needfiles = {}
@@ -449,6 +638,8 b' class cg1unpacker:'
449 638 efiles,
450 639 needfiles,
451 640 addrevisioncb=on_filelog_rev,
641 debug_info=debug_info,
642 delta_base_reuse_policy=delta_base_reuse_policy,
452 643 )
453 644
454 645 if sidedata_helpers:
@@ -567,6 +758,8 b' class cg1unpacker:'
567 758 b'changegroup-runhooks-%020i' % clstart,
568 759 lambda tr: repo._afterlock(runhooks),
569 760 )
761 if debug_info is not None:
762 display_unbundle_debug_info(repo.ui, debug_info)
570 763 finally:
571 764 repo.ui.flush()
572 765 # never return 0 here:
@@ -626,9 +819,24 b' class cg3unpacker(cg2unpacker):'
626 819 protocol_flags = 0
627 820 return node, p1, p2, deltabase, cs, flags, protocol_flags
628 821
629 def _unpackmanifests(self, repo, revmap, trp, prog, addrevisioncb=None):
822 def _unpackmanifests(
823 self,
824 repo,
825 revmap,
826 trp,
827 prog,
828 addrevisioncb=None,
829 debug_info=None,
830 delta_base_reuse_policy=None,
831 ):
630 832 super(cg3unpacker, self)._unpackmanifests(
631 repo, revmap, trp, prog, addrevisioncb=addrevisioncb
833 repo,
834 revmap,
835 trp,
836 prog,
837 addrevisioncb=addrevisioncb,
838 debug_info=debug_info,
839 delta_base_reuse_policy=delta_base_reuse_policy,
632 840 )
633 841 for chunkdata in iter(self.filelogheader, {}):
634 842 # If we get here, there are directory manifests in the changegroup
@@ -636,7 +844,12 b' class cg3unpacker(cg2unpacker):'
636 844 repo.ui.debug(b"adding %s revisions\n" % d)
637 845 deltas = self.deltaiter()
638 846 if not repo.manifestlog.getstorage(d).addgroup(
639 deltas, revmap, trp, addrevisioncb=addrevisioncb
847 deltas,
848 revmap,
849 trp,
850 addrevisioncb=addrevisioncb,
851 debug_info=debug_info,
852 delta_base_reuse_policy=delta_base_reuse_policy,
640 853 ):
641 854 raise error.Abort(_(b"received dir revlog group is empty"))
642 855
@@ -869,6 +1082,7 b' def deltagroup('
869 1082 fullclnodes=None,
870 1083 precomputedellipsis=None,
871 1084 sidedata_helpers=None,
1085 debug_info=None,
872 1086 ):
873 1087 """Calculate deltas for a set of revisions.
874 1088
@@ -978,6 +1192,7 b' def deltagroup('
978 1192 assumehaveparentrevisions=not ellipses,
979 1193 deltamode=deltamode,
980 1194 sidedata_helpers=sidedata_helpers,
1195 debug_info=debug_info,
981 1196 )
982 1197
983 1198 for i, revision in enumerate(revisions):
@@ -1003,6 +1218,187 b' def deltagroup('
1003 1218 progress.complete()
1004 1219
1005 1220
1221 def make_debug_info():
1222 """ "build a "new" debug_info dictionnary
1223
1224 That dictionnary can be used to gather information about the bundle process
1225 """
1226 return {
1227 'revision-total': 0,
1228 'revision-changelog': 0,
1229 'revision-manifest': 0,
1230 'revision-files': 0,
1231 'file-count': 0,
1232 'merge-total': 0,
1233 'available-delta': 0,
1234 'available-full': 0,
1235 'delta-against-prev': 0,
1236 'delta-full': 0,
1237 'delta-against-p1': 0,
1238 'denied-delta-candeltafn': 0,
1239 'denied-base-not-available': 0,
1240 'reused-storage-delta': 0,
1241 'computed-delta': 0,
1242 }
1243
1244
1245 def merge_debug_info(base, other):
1246 """merge the debug information from <other> into <base>
1247
1248 This function can be used to gather lower level information into higher level ones.
1249 """
1250 for key in (
1251 'revision-total',
1252 'revision-changelog',
1253 'revision-manifest',
1254 'revision-files',
1255 'merge-total',
1256 'available-delta',
1257 'available-full',
1258 'delta-against-prev',
1259 'delta-full',
1260 'delta-against-p1',
1261 'denied-delta-candeltafn',
1262 'denied-base-not-available',
1263 'reused-storage-delta',
1264 'computed-delta',
1265 ):
1266 base[key] += other[key]
1267
1268
1269 _KEY_PART_WIDTH = 17
1270
1271
1272 def _dbg_bdl_line(
1273 ui,
1274 indent,
1275 key,
1276 base_value=None,
1277 percentage_base=None,
1278 percentage_key=None,
1279 percentage_ref=None,
1280 extra=None,
1281 ):
1282 """Print one line of debug_bundle_debug_info"""
1283 line = b"DEBUG-BUNDLING: "
1284 line += b' ' * (2 * indent)
1285 key += b":"
1286 if base_value is not None:
1287 assert len(key) + 1 + (2 * indent) <= _KEY_PART_WIDTH
1288 line += key.ljust(_KEY_PART_WIDTH - (2 * indent))
1289 line += b"%10d" % base_value
1290 else:
1291 line += key
1292
1293 if percentage_base is not None:
1294 assert base_value is not None
1295 percentage = base_value * 100 // percentage_base
1296 if percentage_key is not None:
1297 line += b" (%d%% of %s %d)" % (
1298 percentage,
1299 percentage_key,
1300 percentage_ref,
1301 )
1302 else:
1303 line += b" (%d%%)" % percentage
1304
1305 if extra:
1306 line += b" "
1307 line += extra
1308
1309 line += b'\n'
1310 ui.write_err(line)
1311
1312
1313 def display_bundling_debug_info(
1314 ui,
1315 debug_info,
1316 cl_debug_info,
1317 mn_debug_info,
1318 fl_debug_info,
1319 ):
1320 """display debug information gathered during a bundling through `ui`"""
1321 d = debug_info
1322 c = cl_debug_info
1323 m = mn_debug_info
1324 f = fl_debug_info
1325 all_info = [
1326 (b"changelog", b"cl", c),
1327 (b"manifests", b"mn", m),
1328 (b"files", b"fl", f),
1329 ]
1330 _dbg_bdl_line(ui, 0, b'revisions', d['revision-total'])
1331 _dbg_bdl_line(ui, 1, b'changelog', d['revision-changelog'])
1332 _dbg_bdl_line(ui, 1, b'manifest', d['revision-manifest'])
1333 extra = b'(for %d revlogs)' % d['file-count']
1334 _dbg_bdl_line(ui, 1, b'files', d['revision-files'], extra=extra)
1335 if d['merge-total']:
1336 _dbg_bdl_line(ui, 1, b'merge', d['merge-total'], d['revision-total'])
1337 for k, __, v in all_info:
1338 if v['merge-total']:
1339 _dbg_bdl_line(ui, 2, k, v['merge-total'], v['revision-total'])
1340
1341 _dbg_bdl_line(ui, 0, b'deltas')
1342 _dbg_bdl_line(
1343 ui,
1344 1,
1345 b'from-storage',
1346 d['reused-storage-delta'],
1347 percentage_base=d['available-delta'],
1348 percentage_key=b"available",
1349 percentage_ref=d['available-delta'],
1350 )
1351
1352 if d['denied-delta-candeltafn']:
1353 _dbg_bdl_line(ui, 2, b'denied-fn', d['denied-delta-candeltafn'])
1354 for __, k, v in all_info:
1355 if v['denied-delta-candeltafn']:
1356 _dbg_bdl_line(ui, 3, k, v['denied-delta-candeltafn'])
1357
1358 if d['denied-base-not-available']:
1359 _dbg_bdl_line(ui, 2, b'denied-nb', d['denied-base-not-available'])
1360 for k, __, v in all_info:
1361 if v['denied-base-not-available']:
1362 _dbg_bdl_line(ui, 3, k, v['denied-base-not-available'])
1363
1364 if d['computed-delta']:
1365 _dbg_bdl_line(ui, 1, b'computed', d['computed-delta'])
1366
1367 if d['available-full']:
1368 _dbg_bdl_line(
1369 ui,
1370 2,
1371 b'full',
1372 d['delta-full'],
1373 percentage_base=d['available-full'],
1374 percentage_key=b"native",
1375 percentage_ref=d['available-full'],
1376 )
1377 for k, __, v in all_info:
1378 if v['available-full']:
1379 _dbg_bdl_line(
1380 ui,
1381 3,
1382 k,
1383 v['delta-full'],
1384 percentage_base=v['available-full'],
1385 percentage_key=b"native",
1386 percentage_ref=v['available-full'],
1387 )
1388
1389 if d['delta-against-prev']:
1390 _dbg_bdl_line(ui, 2, b'previous', d['delta-against-prev'])
1391 for k, __, v in all_info:
1392 if v['delta-against-prev']:
1393 _dbg_bdl_line(ui, 3, k, v['delta-against-prev'])
1394
1395 if d['delta-against-p1']:
1396 _dbg_bdl_line(ui, 2, b'parent-1', d['delta-against-prev'])
1397 for k, __, v in all_info:
1398 if v['delta-against-p1']:
1399 _dbg_bdl_line(ui, 3, k, v['delta-against-p1'])
1400
1401
1006 1402 class cgpacker:
1007 1403 def __init__(
1008 1404 self,
@@ -1086,13 +1482,21 b' class cgpacker:'
1086 1482 self._verbosenote = lambda s: None
1087 1483
1088 1484 def generate(
1089 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True
1485 self,
1486 commonrevs,
1487 clnodes,
1488 fastpathlinkrev,
1489 source,
1490 changelog=True,
1090 1491 ):
1091 1492 """Yield a sequence of changegroup byte chunks.
1092 1493 If changelog is False, changelog data won't be added to changegroup
1093 1494 """
1094 1495
1496 debug_info = None
1095 1497 repo = self._repo
1498 if repo.ui.configbool(b'debug', b'bundling-stats'):
1499 debug_info = make_debug_info()
1096 1500 cl = repo.changelog
1097 1501
1098 1502 self._verbosenote(_(b'uncompressed size of bundle content:\n'))
@@ -1107,14 +1511,19 b' class cgpacker:'
1107 1511 # correctly advertise its sidedata categories directly.
1108 1512 remote_sidedata = repo._wanted_sidedata
1109 1513 sidedata_helpers = sidedatamod.get_sidedata_helpers(
1110 repo, remote_sidedata
1514 repo,
1515 remote_sidedata,
1111 1516 )
1112 1517
1518 cl_debug_info = None
1519 if debug_info is not None:
1520 cl_debug_info = make_debug_info()
1113 1521 clstate, deltas = self._generatechangelog(
1114 1522 cl,
1115 1523 clnodes,
1116 1524 generate=changelog,
1117 1525 sidedata_helpers=sidedata_helpers,
1526 debug_info=cl_debug_info,
1118 1527 )
1119 1528 for delta in deltas:
1120 1529 for chunk in _revisiondeltatochunks(
@@ -1126,6 +1535,9 b' class cgpacker:'
1126 1535 close = closechunk()
1127 1536 size += len(close)
1128 1537 yield closechunk()
1538 if debug_info is not None:
1539 merge_debug_info(debug_info, cl_debug_info)
1540 debug_info['revision-changelog'] = cl_debug_info['revision-total']
1129 1541
1130 1542 self._verbosenote(_(b'%8.i (changelog)\n') % size)
1131 1543
@@ -1133,6 +1545,9 b' class cgpacker:'
1133 1545 manifests = clstate[b'manifests']
1134 1546 changedfiles = clstate[b'changedfiles']
1135 1547
1548 if debug_info is not None:
1549 debug_info['file-count'] = len(changedfiles)
1550
1136 1551 # We need to make sure that the linkrev in the changegroup refers to
1137 1552 # the first changeset that introduced the manifest or file revision.
1138 1553 # The fastpath is usually safer than the slowpath, because the filelogs
@@ -1156,6 +1571,9 b' class cgpacker:'
1156 1571 fnodes = {} # needed file nodes
1157 1572
1158 1573 size = 0
1574 mn_debug_info = None
1575 if debug_info is not None:
1576 mn_debug_info = make_debug_info()
1159 1577 it = self.generatemanifests(
1160 1578 commonrevs,
1161 1579 clrevorder,
@@ -1165,6 +1583,7 b' class cgpacker:'
1165 1583 source,
1166 1584 clstate[b'clrevtomanifestrev'],
1167 1585 sidedata_helpers=sidedata_helpers,
1586 debug_info=mn_debug_info,
1168 1587 )
1169 1588
1170 1589 for tree, deltas in it:
@@ -1185,6 +1604,9 b' class cgpacker:'
1185 1604 close = closechunk()
1186 1605 size += len(close)
1187 1606 yield close
1607 if debug_info is not None:
1608 merge_debug_info(debug_info, mn_debug_info)
1609 debug_info['revision-manifest'] = mn_debug_info['revision-total']
1188 1610
1189 1611 self._verbosenote(_(b'%8.i (manifests)\n') % size)
1190 1612 yield self._manifestsend
@@ -1199,6 +1621,9 b' class cgpacker:'
1199 1621 manifests.clear()
1200 1622 clrevs = {cl.rev(x) for x in clnodes}
1201 1623
1624 fl_debug_info = None
1625 if debug_info is not None:
1626 fl_debug_info = make_debug_info()
1202 1627 it = self.generatefiles(
1203 1628 changedfiles,
1204 1629 commonrevs,
@@ -1208,6 +1633,7 b' class cgpacker:'
1208 1633 fnodes,
1209 1634 clrevs,
1210 1635 sidedata_helpers=sidedata_helpers,
1636 debug_info=fl_debug_info,
1211 1637 )
1212 1638
1213 1639 for path, deltas in it:
@@ -1230,12 +1656,29 b' class cgpacker:'
1230 1656 self._verbosenote(_(b'%8.i %s\n') % (size, path))
1231 1657
1232 1658 yield closechunk()
1659 if debug_info is not None:
1660 merge_debug_info(debug_info, fl_debug_info)
1661 debug_info['revision-files'] = fl_debug_info['revision-total']
1662
1663 if debug_info is not None:
1664 display_bundling_debug_info(
1665 repo.ui,
1666 debug_info,
1667 cl_debug_info,
1668 mn_debug_info,
1669 fl_debug_info,
1670 )
1233 1671
1234 1672 if clnodes:
1235 1673 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source)
1236 1674
1237 1675 def _generatechangelog(
1238 self, cl, nodes, generate=True, sidedata_helpers=None
1676 self,
1677 cl,
1678 nodes,
1679 generate=True,
1680 sidedata_helpers=None,
1681 debug_info=None,
1239 1682 ):
1240 1683 """Generate data for changelog chunks.
1241 1684
@@ -1332,6 +1775,7 b' class cgpacker:'
1332 1775 fullclnodes=self._fullclnodes,
1333 1776 precomputedellipsis=self._precomputedellipsis,
1334 1777 sidedata_helpers=sidedata_helpers,
1778 debug_info=debug_info,
1335 1779 )
1336 1780
1337 1781 return state, gen
@@ -1346,6 +1790,7 b' class cgpacker:'
1346 1790 source,
1347 1791 clrevtolocalrev,
1348 1792 sidedata_helpers=None,
1793 debug_info=None,
1349 1794 ):
1350 1795 """Returns an iterator of changegroup chunks containing manifests.
1351 1796
@@ -1444,6 +1889,7 b' class cgpacker:'
1444 1889 fullclnodes=self._fullclnodes,
1445 1890 precomputedellipsis=self._precomputedellipsis,
1446 1891 sidedata_helpers=sidedata_helpers,
1892 debug_info=debug_info,
1447 1893 )
1448 1894
1449 1895 if not self._oldmatcher.visitdir(store.tree[:-1]):
@@ -1483,6 +1929,7 b' class cgpacker:'
1483 1929 fnodes,
1484 1930 clrevs,
1485 1931 sidedata_helpers=None,
1932 debug_info=None,
1486 1933 ):
1487 1934 changedfiles = [
1488 1935 f
@@ -1578,6 +2025,7 b' class cgpacker:'
1578 2025 fullclnodes=self._fullclnodes,
1579 2026 precomputedellipsis=self._precomputedellipsis,
1580 2027 sidedata_helpers=sidedata_helpers,
2028 debug_info=debug_info,
1581 2029 )
1582 2030
1583 2031 yield fname, deltas
@@ -1867,7 +2315,12 b' def _changegroupinfo(repo, nodes, source'
1867 2315
1868 2316
1869 2317 def makechangegroup(
1870 repo, outgoing, version, source, fastpath=False, bundlecaps=None
2318 repo,
2319 outgoing,
2320 version,
2321 source,
2322 fastpath=False,
2323 bundlecaps=None,
1871 2324 ):
1872 2325 cgstream = makestream(
1873 2326 repo,
@@ -1917,7 +2370,12 b' def makestream('
1917 2370
1918 2371 repo.hook(b'preoutgoing', throw=True, source=source)
1919 2372 _changegroupinfo(repo, csets, source)
1920 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
2373 return bundler.generate(
2374 commonrevs,
2375 csets,
2376 fastpathlinkrev,
2377 source,
2378 )
1921 2379
1922 2380
1923 2381 def _addchangegroupfiles(
@@ -1928,6 +2386,8 b' def _addchangegroupfiles('
1928 2386 expectedfiles,
1929 2387 needfiles,
1930 2388 addrevisioncb=None,
2389 debug_info=None,
2390 delta_base_reuse_policy=None,
1931 2391 ):
1932 2392 revisions = 0
1933 2393 files = 0
@@ -1948,6 +2408,8 b' def _addchangegroupfiles('
1948 2408 revmap,
1949 2409 trp,
1950 2410 addrevisioncb=addrevisioncb,
2411 debug_info=debug_info,
2412 delta_base_reuse_policy=delta_base_reuse_policy,
1951 2413 )
1952 2414 if not added:
1953 2415 raise error.Abort(_(b"received file revlog group is empty"))
@@ -11,6 +11,15 b' import errno'
11 11 import os
12 12 import re
13 13
14 from typing import (
15 Any,
16 AnyStr,
17 Dict,
18 Iterable,
19 Optional,
20 cast,
21 )
22
14 23 from .i18n import _
15 24 from .node import (
16 25 hex,
@@ -29,7 +38,6 b' from . import ('
29 38 changelog,
30 39 copies,
31 40 crecord as crecordmod,
32 dirstateguard,
33 41 encoding,
34 42 error,
35 43 formatter,
@@ -65,14 +73,10 b' from .revlogutils import ('
65 73 )
66 74
67 75 if pycompat.TYPE_CHECKING:
68 from typing import (
69 Any,
70 Dict,
76 from . import (
77 ui as uimod,
71 78 )
72 79
73 for t in (Any, Dict):
74 assert t
75
76 80 stringio = util.stringio
77 81
78 82 # templates of common command options
@@ -269,13 +273,16 b' debugrevlogopts = ['
269 273 _linebelow = b"^HG: ------------------------ >8 ------------------------$"
270 274
271 275
272 def check_at_most_one_arg(opts, *args):
276 def check_at_most_one_arg(
277 opts: Dict[AnyStr, Any],
278 *args: AnyStr,
279 ) -> Optional[AnyStr]:
273 280 """abort if more than one of the arguments are in opts
274 281
275 282 Returns the unique argument or None if none of them were specified.
276 283 """
277 284
278 def to_display(name):
285 def to_display(name: AnyStr) -> bytes:
279 286 return pycompat.sysbytes(name).replace(b'_', b'-')
280 287
281 288 previous = None
@@ -290,7 +297,11 b' def check_at_most_one_arg(opts, *args):'
290 297 return previous
291 298
292 299
293 def check_incompatible_arguments(opts, first, others):
300 def check_incompatible_arguments(
301 opts: Dict[AnyStr, Any],
302 first: AnyStr,
303 others: Iterable[AnyStr],
304 ) -> None:
294 305 """abort if the first argument is given along with any of the others
295 306
296 307 Unlike check_at_most_one_arg(), `others` are not mutually exclusive
@@ -300,7 +311,7 b' def check_incompatible_arguments(opts, f'
300 311 check_at_most_one_arg(opts, first, other)
301 312
302 313
303 def resolve_commit_options(ui, opts):
314 def resolve_commit_options(ui: "uimod.ui", opts: Dict[str, Any]) -> bool:
304 315 """modify commit options dict to handle related options
305 316
306 317 The return value indicates that ``rewrite.update-timestamp`` is the reason
@@ -327,7 +338,7 b' def resolve_commit_options(ui, opts):'
327 338 return datemaydiffer
328 339
329 340
330 def check_note_size(opts):
341 def check_note_size(opts: Dict[str, Any]) -> None:
331 342 """make sure note is of valid format"""
332 343
333 344 note = opts.get('note')
@@ -638,7 +649,7 b' def dorecord('
638 649 # already called within a `pendingchange`, However we
639 650 # are taking a shortcut here in order to be able to
640 651 # quickly deprecated the older API.
641 with dirstate.parentchange():
652 with dirstate.changing_parents(repo):
642 653 dirstate.update_file(
643 654 realname,
644 655 p1_tracked=True,
@@ -1115,12 +1126,12 b' def bailifchanged(repo, merge=True, hint'
1115 1126 ctx.sub(s).bailifchanged(hint=hint)
1116 1127
1117 1128
1118 def logmessage(ui, opts):
1129 def logmessage(ui: "uimod.ui", opts: Dict[bytes, Any]) -> Optional[bytes]:
1119 1130 """get the log message according to -m and -l option"""
1120 1131
1121 1132 check_at_most_one_arg(opts, b'message', b'logfile')
1122 1133
1123 message = opts.get(b'message')
1134 message = cast(Optional[bytes], opts.get(b'message'))
1124 1135 logfile = opts.get(b'logfile')
1125 1136
1126 1137 if not message and logfile:
@@ -1465,7 +1476,7 b' def openrevlog(repo, cmd, file_, opts):'
1465 1476 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1466 1477
1467 1478
1468 def copy(ui, repo, pats, opts, rename=False):
1479 def copy(ui, repo, pats, opts: Dict[bytes, Any], rename=False):
1469 1480 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1470 1481
1471 1482 # called with the repo lock held
@@ -1532,7 +1543,7 b' def copy(ui, repo, pats, opts, rename=Fa'
1532 1543 new_node = mem_ctx.commit()
1533 1544
1534 1545 if repo.dirstate.p1() == ctx.node():
1535 with repo.dirstate.parentchange():
1546 with repo.dirstate.changing_parents(repo):
1536 1547 scmutil.movedirstate(repo, repo[new_node])
1537 1548 replacements = {ctx.node(): [new_node]}
1538 1549 scmutil.cleanupnodes(
@@ -1625,7 +1636,7 b' def copy(ui, repo, pats, opts, rename=Fa'
1625 1636 new_node = mem_ctx.commit()
1626 1637
1627 1638 if repo.dirstate.p1() == ctx.node():
1628 with repo.dirstate.parentchange():
1639 with repo.dirstate.changing_parents(repo):
1629 1640 scmutil.movedirstate(repo, repo[new_node])
1630 1641 replacements = {ctx.node(): [new_node]}
1631 1642 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
@@ -2778,7 +2789,7 b' def cat(ui, repo, ctx, matcher, basefm, '
2778 2789 basefm,
2779 2790 fntemplate,
2780 2791 subprefix,
2781 **pycompat.strkwargs(opts)
2792 **pycompat.strkwargs(opts),
2782 2793 ):
2783 2794 err = 0
2784 2795 except error.RepoLookupError:
@@ -2789,29 +2800,135 b' def cat(ui, repo, ctx, matcher, basefm, '
2789 2800 return err
2790 2801
2791 2802
2803 class _AddRemoveContext:
2804 """a small (hacky) context to deal with lazy opening of context
2805
2806 This is to be used in the `commit` function right below. This deals with
2807 lazily open a `changing_files` context inside a `transaction` that span the
2808 full commit operation.
2809
2810 We need :
2811 - a `changing_files` context to wrap the dirstate change within the
2812 "addremove" operation,
2813 - a transaction to make sure these change are not written right after the
2814 addremove, but when the commit operation succeed.
2815
2816 However it get complicated because:
2817 - opening a transaction "this early" shuffle hooks order, especially the
2818 `precommit` one happening after the `pretxtopen` one which I am not too
2819 enthusiastic about.
2820 - the `mq` extensions + the `record` extension stacks many layers of call
2821 to implement `qrefresh --interactive` and this result with `mq` calling a
2822 `strip` in the middle of this function. Which prevent the existence of
2823 transaction wrapping all of its function code. (however, `qrefresh` never
2824 call the `addremove` bits.
2825 - the largefile extensions (and maybe other extensions?) wraps `addremove`
2826 so slicing `addremove` in smaller bits is a complex endeavour.
2827
2828 So I eventually took a this shortcut that open the transaction if we
2829 actually needs it, not disturbing much of the rest of the code.
2830
2831 It will result in some hooks order change for `hg commit --addremove`,
2832 however it seems a corner case enough to ignore that for now (hopefully).
2833
2834 Notes that None of the above problems seems insurmountable, however I have
2835 been fighting with this specific piece of code for a couple of day already
2836 and I need a solution to keep moving forward on the bigger work around
2837 `changing_files` context that is being introduced at the same time as this
2838 hack.
2839
2840 Each problem seems to have a solution:
2841 - the hook order issue could be solved by refactoring the many-layer stack
2842 that currently composes a commit and calling them earlier,
2843 - the mq issue could be solved by refactoring `mq` so that the final strip
2844 is done after transaction closure. Be warned that the mq code is quite
2845 antic however.
2846 - large-file could be reworked in parallel of the `addremove` to be
2847 friendlier to this.
2848
2849 However each of these tasks are too much a diversion right now. In addition
2850 they will be much easier to undertake when the `changing_files` dust has
2851 settled."""
2852
2853 def __init__(self, repo):
2854 self._repo = repo
2855 self._transaction = None
2856 self._dirstate_context = None
2857 self._state = None
2858
2859 def __enter__(self):
2860 assert self._state is None
2861 self._state = True
2862 return self
2863
2864 def open_transaction(self):
2865 """open a `transaction` and `changing_files` context
2866
2867 Call this when you know that change to the dirstate will be needed and
2868 we need to open the transaction early
2869
2870 This will also open the dirstate `changing_files` context, so you should
2871 call `close_dirstate_context` when the distate changes are done.
2872 """
2873 assert self._state is not None
2874 if self._transaction is None:
2875 self._transaction = self._repo.transaction(b'commit')
2876 self._transaction.__enter__()
2877 if self._dirstate_context is None:
2878 self._dirstate_context = self._repo.dirstate.changing_files(
2879 self._repo
2880 )
2881 self._dirstate_context.__enter__()
2882
2883 def close_dirstate_context(self):
2884 """close the change_files if any
2885
2886 Call this after the (potential) `open_transaction` call to close the
2887 (potential) changing_files context.
2888 """
2889 if self._dirstate_context is not None:
2890 self._dirstate_context.__exit__(None, None, None)
2891 self._dirstate_context = None
2892
2893 def __exit__(self, *args):
2894 if self._dirstate_context is not None:
2895 self._dirstate_context.__exit__(*args)
2896 if self._transaction is not None:
2897 self._transaction.__exit__(*args)
2898
2899
2792 2900 def commit(ui, repo, commitfunc, pats, opts):
2793 2901 '''commit the specified files or all outstanding changes'''
2794 2902 date = opts.get(b'date')
2795 2903 if date:
2796 2904 opts[b'date'] = dateutil.parsedate(date)
2797 message = logmessage(ui, opts)
2798 matcher = scmutil.match(repo[None], pats, opts)
2799
2800 dsguard = None
2801 # extract addremove carefully -- this function can be called from a command
2802 # that doesn't support addremove
2803 if opts.get(b'addremove'):
2804 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2805 with dsguard or util.nullcontextmanager():
2806 if dsguard:
2807 relative = scmutil.anypats(pats, opts)
2808 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2809 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2810 raise error.Abort(
2811 _(b"failed to mark all new/missing files as added/removed")
2905
2906 with repo.wlock(), repo.lock():
2907 message = logmessage(ui, opts)
2908 matcher = scmutil.match(repo[None], pats, opts)
2909
2910 with _AddRemoveContext(repo) as c:
2911 # extract addremove carefully -- this function can be called from a
2912 # command that doesn't support addremove
2913 if opts.get(b'addremove'):
2914 relative = scmutil.anypats(pats, opts)
2915 uipathfn = scmutil.getuipathfn(
2916 repo,
2917 legacyrelativevalue=relative,
2812 2918 )
2813
2814 return commitfunc(ui, repo, message, matcher, opts)
2919 r = scmutil.addremove(
2920 repo,
2921 matcher,
2922 b"",
2923 uipathfn,
2924 opts,
2925 open_tr=c.open_transaction,
2926 )
2927 m = _(b"failed to mark all new/missing files as added/removed")
2928 if r != 0:
2929 raise error.Abort(m)
2930 c.close_dirstate_context()
2931 return commitfunc(ui, repo, message, matcher, opts)
2815 2932
2816 2933
2817 2934 def samefile(f, ctx1, ctx2):
@@ -2826,7 +2943,7 b' def samefile(f, ctx1, ctx2):'
2826 2943 return f not in ctx2.manifest()
2827 2944
2828 2945
2829 def amend(ui, repo, old, extra, pats, opts):
2946 def amend(ui, repo, old, extra, pats, opts: Dict[str, Any]):
2830 2947 # avoid cycle context -> subrepo -> cmdutil
2831 2948 from . import context
2832 2949
@@ -2880,12 +2997,13 b' def amend(ui, repo, old, extra, pats, op'
2880 2997 matcher = scmutil.match(wctx, pats, opts)
2881 2998 relative = scmutil.anypats(pats, opts)
2882 2999 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2883 if opts.get(b'addremove') and scmutil.addremove(
2884 repo, matcher, b"", uipathfn, opts
2885 ):
2886 raise error.Abort(
2887 _(b"failed to mark all new/missing files as added/removed")
2888 )
3000 if opts.get(b'addremove'):
3001 with repo.dirstate.changing_files(repo):
3002 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
3003 m = _(
3004 b"failed to mark all new/missing files as added/removed"
3005 )
3006 raise error.Abort(m)
2889 3007
2890 3008 # Check subrepos. This depends on in-place wctx._status update in
2891 3009 # subrepo.precommit(). To minimize the risk of this hack, we do
@@ -3019,10 +3137,12 b' def amend(ui, repo, old, extra, pats, op'
3019 3137 commitphase = None
3020 3138 if opts.get(b'secret'):
3021 3139 commitphase = phases.secret
3140 elif opts.get(b'draft'):
3141 commitphase = phases.draft
3022 3142 newid = repo.commitctx(new)
3023 3143 ms.reset()
3024 3144
3025 with repo.dirstate.parentchange():
3145 with repo.dirstate.changing_parents(repo):
3026 3146 # Reroute the working copy parent to the new changeset
3027 3147 repo.setparents(newid, repo.nullid)
3028 3148
@@ -3285,7 +3405,7 b' def revert(ui, repo, ctx, *pats, **opts)'
3285 3405 names = {}
3286 3406 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3287 3407
3288 with repo.wlock():
3408 with repo.wlock(), repo.dirstate.changing_files(repo):
3289 3409 ## filling of the `names` mapping
3290 3410 # walk dirstate to fill `names`
3291 3411
@@ -13,6 +13,7 b' import sys'
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 nullid,
16 17 nullrev,
17 18 short,
18 19 wdirrev,
@@ -28,7 +29,6 b' from . import ('
28 29 copies,
29 30 debugcommands as debugcommandsmod,
30 31 destutil,
31 dirstateguard,
32 32 discovery,
33 33 encoding,
34 34 error,
@@ -252,10 +252,11 b' def add(ui, repo, *pats, **opts):'
252 252 Returns 0 if all files are successfully added.
253 253 """
254 254
255 m = scmutil.match(repo[None], pats, pycompat.byteskwargs(opts))
256 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
257 rejected = cmdutil.add(ui, repo, m, b"", uipathfn, False, **opts)
258 return rejected and 1 or 0
255 with repo.wlock(), repo.dirstate.changing_files(repo):
256 m = scmutil.match(repo[None], pats, pycompat.byteskwargs(opts))
257 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
258 rejected = cmdutil.add(ui, repo, m, b"", uipathfn, False, **opts)
259 return rejected and 1 or 0
259 260
260 261
261 262 @command(
@@ -330,10 +331,11 b' def addremove(ui, repo, *pats, **opts):'
330 331 opts = pycompat.byteskwargs(opts)
331 332 if not opts.get(b'similarity'):
332 333 opts[b'similarity'] = b'100'
333 matcher = scmutil.match(repo[None], pats, opts)
334 relative = scmutil.anypats(pats, opts)
335 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
336 return scmutil.addremove(repo, matcher, b"", uipathfn, opts)
334 with repo.wlock(), repo.dirstate.changing_files(repo):
335 matcher = scmutil.match(repo[None], pats, opts)
336 relative = scmutil.anypats(pats, opts)
337 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
338 return scmutil.addremove(repo, matcher, b"", uipathfn, opts)
337 339
338 340
339 341 @command(
@@ -822,7 +824,7 b' def _dobackout(ui, repo, node=None, rev='
822 824 bheads = repo.branchheads(branch)
823 825 rctx = scmutil.revsingle(repo, hex(parent))
824 826 if not opts.get(b'merge') and op1 != node:
825 with dirstateguard.dirstateguard(repo, b'backout'):
827 with repo.transaction(b"backout"):
826 828 overrides = {(b'ui', b'forcemerge'): opts.get(b'tool', b'')}
827 829 with ui.configoverride(overrides, b'backout'):
828 830 stats = mergemod.back_out(ctx, parent=repo[parent])
@@ -1635,7 +1637,7 b' def bundle(ui, repo, fname, *dests, **op'
1635 1637 missing = set()
1636 1638 excluded = set()
1637 1639 for path in urlutil.get_push_paths(repo, ui, dests):
1638 other = hg.peer(repo, opts, path.rawloc)
1640 other = hg.peer(repo, opts, path)
1639 1641 if revs is not None:
1640 1642 hex_revs = [repo[r].hex() for r in revs]
1641 1643 else:
@@ -2008,6 +2010,7 b' def clone(ui, source, dest=None, **opts)'
2008 2010 (b'', b'close-branch', None, _(b'mark a branch head as closed')),
2009 2011 (b'', b'amend', None, _(b'amend the parent of the working directory')),
2010 2012 (b's', b'secret', None, _(b'use the secret phase for committing')),
2013 (b'', b'draft', None, _(b'use the draft phase for committing')),
2011 2014 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
2012 2015 (
2013 2016 b'',
@@ -2082,6 +2085,8 b' def commit(ui, repo, *pats, **opts):'
2082 2085
2083 2086 hg commit --amend --date now
2084 2087 """
2088 cmdutil.check_at_most_one_arg(opts, 'draft', 'secret')
2089 cmdutil.check_incompatible_arguments(opts, 'subrepos', ['amend'])
2085 2090 with repo.wlock(), repo.lock():
2086 2091 return _docommit(ui, repo, *pats, **opts)
2087 2092
@@ -2097,7 +2102,6 b' def _docommit(ui, repo, *pats, **opts):'
2097 2102 return 1 if ret == 0 else ret
2098 2103
2099 2104 if opts.get('subrepos'):
2100 cmdutil.check_incompatible_arguments(opts, 'subrepos', ['amend'])
2101 2105 # Let --subrepos on the command line override config setting.
2102 2106 ui.setconfig(b'ui', b'commitsubrepos', True, b'commit')
2103 2107
@@ -2174,6 +2178,8 b' def _docommit(ui, repo, *pats, **opts):'
2174 2178 overrides = {}
2175 2179 if opts.get(b'secret'):
2176 2180 overrides[(b'phases', b'new-commit')] = b'secret'
2181 elif opts.get(b'draft'):
2182 overrides[(b'phases', b'new-commit')] = b'draft'
2177 2183
2178 2184 baseui = repo.baseui
2179 2185 with baseui.configoverride(overrides, b'commit'):
@@ -2491,7 +2497,19 b' def copy(ui, repo, *pats, **opts):'
2491 2497 Returns 0 on success, 1 if errors are encountered.
2492 2498 """
2493 2499 opts = pycompat.byteskwargs(opts)
2494 with repo.wlock():
2500
2501 context = repo.dirstate.changing_files
2502 rev = opts.get(b'at_rev')
2503 ctx = None
2504 if rev:
2505 ctx = logcmdutil.revsingle(repo, rev)
2506 if ctx.rev() is not None:
2507
2508 def context(repo):
2509 return util.nullcontextmanager()
2510
2511 opts[b'at_rev'] = ctx.rev()
2512 with repo.wlock(), context(repo):
2495 2513 return cmdutil.copy(ui, repo, pats, opts)
2496 2514
2497 2515
@@ -2960,19 +2978,20 b' def forget(ui, repo, *pats, **opts):'
2960 2978 if not pats:
2961 2979 raise error.InputError(_(b'no files specified'))
2962 2980
2963 m = scmutil.match(repo[None], pats, opts)
2964 dryrun, interactive = opts.get(b'dry_run'), opts.get(b'interactive')
2965 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2966 rejected = cmdutil.forget(
2967 ui,
2968 repo,
2969 m,
2970 prefix=b"",
2971 uipathfn=uipathfn,
2972 explicitonly=False,
2973 dryrun=dryrun,
2974 interactive=interactive,
2975 )[0]
2981 with repo.wlock(), repo.dirstate.changing_files(repo):
2982 m = scmutil.match(repo[None], pats, opts)
2983 dryrun, interactive = opts.get(b'dry_run'), opts.get(b'interactive')
2984 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2985 rejected = cmdutil.forget(
2986 ui,
2987 repo,
2988 m,
2989 prefix=b"",
2990 uipathfn=uipathfn,
2991 explicitonly=False,
2992 dryrun=dryrun,
2993 interactive=interactive,
2994 )[0]
2976 2995 return rejected and 1 or 0
2977 2996
2978 2997
@@ -3911,12 +3930,11 b' def identify('
3911 3930 peer = None
3912 3931 try:
3913 3932 if source:
3914 source, branches = urlutil.get_unique_pull_path(
3915 b'identify', repo, ui, source
3916 )
3933 path = urlutil.get_unique_pull_path_obj(b'identify', ui, source)
3917 3934 # only pass ui when no repo
3918 peer = hg.peer(repo or ui, opts, source)
3935 peer = hg.peer(repo or ui, opts, path)
3919 3936 repo = peer.local()
3937 branches = (path.branch, [])
3920 3938 revs, checkout = hg.addbranchrevs(repo, peer, branches, None)
3921 3939
3922 3940 fm = ui.formatter(b'identify', opts)
@@ -4229,12 +4247,10 b' def import_(ui, repo, patch1=None, *patc'
4229 4247 if not opts.get(b'no_commit'):
4230 4248 lock = repo.lock
4231 4249 tr = lambda: repo.transaction(b'import')
4232 dsguard = util.nullcontextmanager
4233 4250 else:
4234 4251 lock = util.nullcontextmanager
4235 4252 tr = util.nullcontextmanager
4236 dsguard = lambda: dirstateguard.dirstateguard(repo, b'import')
4237 with lock(), tr(), dsguard():
4253 with lock(), tr():
4238 4254 parents = repo[None].parents()
4239 4255 for patchurl in patches:
4240 4256 if patchurl == b'-':
@@ -4383,17 +4399,15 b' def incoming(ui, repo, source=b"default"'
4383 4399 if opts.get(b'bookmarks'):
4384 4400 srcs = urlutil.get_pull_paths(repo, ui, [source])
4385 4401 for path in srcs:
4386 source, branches = urlutil.parseurl(
4387 path.rawloc, opts.get(b'branch')
4388 )
4389 other = hg.peer(repo, opts, source)
4402 # XXX the "branches" options are not used. Should it be used?
4403 other = hg.peer(repo, opts, path)
4390 4404 try:
4391 4405 if b'bookmarks' not in other.listkeys(b'namespaces'):
4392 4406 ui.warn(_(b"remote doesn't support bookmarks\n"))
4393 4407 return 0
4394 4408 ui.pager(b'incoming')
4395 4409 ui.status(
4396 _(b'comparing with %s\n') % urlutil.hidepassword(source)
4410 _(b'comparing with %s\n') % urlutil.hidepassword(path.loc)
4397 4411 )
4398 4412 return bookmarks.incoming(
4399 4413 ui, repo, other, mode=path.bookmarks_mode
@@ -4426,7 +4440,7 b' def init(ui, dest=b".", **opts):'
4426 4440 Returns 0 on success.
4427 4441 """
4428 4442 opts = pycompat.byteskwargs(opts)
4429 path = urlutil.get_clone_path(ui, dest)[1]
4443 path = urlutil.get_clone_path_obj(ui, dest)
4430 4444 peer = hg.peer(ui, opts, path, create=True)
4431 4445 peer.close()
4432 4446
@@ -5038,14 +5052,13 b' def outgoing(ui, repo, *dests, **opts):'
5038 5052 opts = pycompat.byteskwargs(opts)
5039 5053 if opts.get(b'bookmarks'):
5040 5054 for path in urlutil.get_push_paths(repo, ui, dests):
5041 dest = path.pushloc or path.loc
5042 other = hg.peer(repo, opts, dest)
5055 other = hg.peer(repo, opts, path)
5043 5056 try:
5044 5057 if b'bookmarks' not in other.listkeys(b'namespaces'):
5045 5058 ui.warn(_(b"remote doesn't support bookmarks\n"))
5046 5059 return 0
5047 5060 ui.status(
5048 _(b'comparing with %s\n') % urlutil.hidepassword(dest)
5061 _(b'comparing with %s\n') % urlutil.hidepassword(path.loc)
5049 5062 )
5050 5063 ui.pager(b'outgoing')
5051 5064 return bookmarks.outgoing(ui, repo, other)
@@ -5434,12 +5447,12 b' def pull(ui, repo, *sources, **opts):'
5434 5447 raise error.InputError(msg, hint=hint)
5435 5448
5436 5449 for path in urlutil.get_pull_paths(repo, ui, sources):
5437 source, branches = urlutil.parseurl(path.rawloc, opts.get(b'branch'))
5438 ui.status(_(b'pulling from %s\n') % urlutil.hidepassword(source))
5450 ui.status(_(b'pulling from %s\n') % urlutil.hidepassword(path.loc))
5439 5451 ui.flush()
5440 other = hg.peer(repo, opts, source)
5452 other = hg.peer(repo, opts, path)
5441 5453 update_conflict = None
5442 5454 try:
5455 branches = (path.branch, opts.get(b'branch', []))
5443 5456 revs, checkout = hg.addbranchrevs(
5444 5457 repo, other, branches, opts.get(b'rev')
5445 5458 )
@@ -5515,8 +5528,12 b' def pull(ui, repo, *sources, **opts):'
5515 5528 elif opts.get(b'branch'):
5516 5529 brev = opts[b'branch'][0]
5517 5530 else:
5518 brev = branches[0]
5519 repo._subtoppath = source
5531 brev = path.branch
5532
5533 # XXX path: we are losing the `path` object here. Keeping it
5534 # would be valuable. For example as a "variant" as we do
5535 # for pushes.
5536 repo._subtoppath = path.loc
5520 5537 try:
5521 5538 update_conflict = postincoming(
5522 5539 ui, repo, modheads, opts.get(b'update'), checkout, brev
@@ -5766,7 +5783,7 b' def push(ui, repo, *dests, **opts):'
5766 5783 some_pushed = False
5767 5784 result = 0
5768 5785 for path in urlutil.get_push_paths(repo, ui, dests):
5769 dest = path.pushloc or path.loc
5786 dest = path.loc
5770 5787 branches = (path.branch, opts.get(b'branch') or [])
5771 5788 ui.status(_(b'pushing to %s\n') % urlutil.hidepassword(dest))
5772 5789 revs, checkout = hg.addbranchrevs(
@@ -5940,12 +5957,13 b' def remove(ui, repo, *pats, **opts):'
5940 5957 if not pats and not after:
5941 5958 raise error.InputError(_(b'no files specified'))
5942 5959
5943 m = scmutil.match(repo[None], pats, opts)
5944 subrepos = opts.get(b'subrepos')
5945 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
5946 return cmdutil.remove(
5947 ui, repo, m, b"", uipathfn, after, force, subrepos, dryrun=dryrun
5948 )
5960 with repo.wlock(), repo.dirstate.changing_files(repo):
5961 m = scmutil.match(repo[None], pats, opts)
5962 subrepos = opts.get(b'subrepos')
5963 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
5964 return cmdutil.remove(
5965 ui, repo, m, b"", uipathfn, after, force, subrepos, dryrun=dryrun
5966 )
5949 5967
5950 5968
5951 5969 @command(
@@ -5994,7 +6012,18 b' def rename(ui, repo, *pats, **opts):'
5994 6012 Returns 0 on success, 1 if errors are encountered.
5995 6013 """
5996 6014 opts = pycompat.byteskwargs(opts)
5997 with repo.wlock():
6015 context = repo.dirstate.changing_files
6016 rev = opts.get(b'at_rev')
6017 ctx = None
6018 if rev:
6019 ctx = logcmdutil.revsingle(repo, rev)
6020 if ctx.rev() is not None:
6021
6022 def context(repo):
6023 return util.nullcontextmanager()
6024
6025 opts[b'at_rev'] = ctx.rev()
6026 with repo.wlock(), context(repo):
5998 6027 return cmdutil.copy(ui, repo, pats, opts, rename=True)
5999 6028
6000 6029
@@ -6260,7 +6289,7 b' def resolve(ui, repo, *pats, **opts):'
6260 6289 #
6261 6290 # All this should eventually happens, but in the mean time, we use this
6262 6291 # context manager slightly out of the context it should be.
6263 with repo.dirstate.parentchange():
6292 with repo.dirstate.changing_parents(repo):
6264 6293 mergestatemod.recordupdates(repo, ms.actions(), branchmerge, None)
6265 6294
6266 6295 if not didwork and pats:
@@ -7252,23 +7281,22 b' def summary(ui, repo, **opts):'
7252 7281 # XXX We should actually skip this if no default is specified, instead
7253 7282 # of passing "default" which will resolve as "./default/" if no default
7254 7283 # path is defined.
7255 source, branches = urlutil.get_unique_pull_path(
7256 b'summary', repo, ui, b'default'
7257 )
7258 sbranch = branches[0]
7284 path = urlutil.get_unique_pull_path_obj(b'summary', ui, b'default')
7285 sbranch = path.branch
7259 7286 try:
7260 other = hg.peer(repo, {}, source)
7287 other = hg.peer(repo, {}, path)
7261 7288 except error.RepoError:
7262 7289 if opts.get(b'remote'):
7263 7290 raise
7264 return source, sbranch, None, None, None
7291 return path.loc, sbranch, None, None, None
7292 branches = (path.branch, [])
7265 7293 revs, checkout = hg.addbranchrevs(repo, other, branches, None)
7266 7294 if revs:
7267 7295 revs = [other.lookup(rev) for rev in revs]
7268 ui.debug(b'comparing with %s\n' % urlutil.hidepassword(source))
7296 ui.debug(b'comparing with %s\n' % urlutil.hidepassword(path.loc))
7269 7297 with repo.ui.silent():
7270 7298 commoninc = discovery.findcommonincoming(repo, other, heads=revs)
7271 return source, sbranch, other, commoninc, commoninc[1]
7299 return path.loc, sbranch, other, commoninc, commoninc[1]
7272 7300
7273 7301 if needsincoming:
7274 7302 source, sbranch, sother, commoninc, incoming = getincoming()
@@ -7284,9 +7312,10 b' def summary(ui, repo, **opts):'
7284 7312 d = b'default-push'
7285 7313 elif b'default' in ui.paths:
7286 7314 d = b'default'
7315 path = None
7287 7316 if d is not None:
7288 7317 path = urlutil.get_unique_push_path(b'summary', repo, ui, d)
7289 dest = path.pushloc or path.loc
7318 dest = path.loc
7290 7319 dbranch = path.branch
7291 7320 else:
7292 7321 dest = b'default'
@@ -7294,7 +7323,7 b' def summary(ui, repo, **opts):'
7294 7323 revs, checkout = hg.addbranchrevs(repo, repo, (dbranch, []), None)
7295 7324 if source != dest:
7296 7325 try:
7297 dother = hg.peer(repo, {}, dest)
7326 dother = hg.peer(repo, {}, path if path is not None else dest)
7298 7327 except error.RepoError:
7299 7328 if opts.get(b'remote'):
7300 7329 raise
@@ -7472,8 +7501,11 b' def tag(ui, repo, name1, *names, **opts)'
7472 7501 )
7473 7502 node = logcmdutil.revsingle(repo, rev_).node()
7474 7503
7504 # don't allow tagging the null rev or the working directory
7475 7505 if node is None:
7476 7506 raise error.InputError(_(b"cannot tag working directory"))
7507 elif not opts.get(b'remove') and node == nullid:
7508 raise error.InputError(_(b"cannot tag null revision"))
7477 7509
7478 7510 if not message:
7479 7511 # we don't translate commit messages
@@ -7494,13 +7526,6 b' def tag(ui, repo, name1, *names, **opts)'
7494 7526 editform=editform, **pycompat.strkwargs(opts)
7495 7527 )
7496 7528
7497 # don't allow tagging the null rev
7498 if (
7499 not opts.get(b'remove')
7500 and logcmdutil.revsingle(repo, rev_).rev() == nullrev
7501 ):
7502 raise error.InputError(_(b"cannot tag null revision"))
7503
7504 7529 tagsmod.tag(
7505 7530 repo,
7506 7531 names,
@@ -588,6 +588,18 b' coreconfigitem('
588 588 b'revlog.debug-delta',
589 589 default=False,
590 590 )
591 # display extra information about the bundling process
592 coreconfigitem(
593 b'debug',
594 b'bundling-stats',
595 default=False,
596 )
597 # display extra information about the unbundling process
598 coreconfigitem(
599 b'debug',
600 b'unbundling-stats',
601 default=False,
602 )
591 603 coreconfigitem(
592 604 b'defaults',
593 605 b'.*',
@@ -734,6 +746,14 b' coreconfigitem('
734 746 b'discovery.exchange-heads',
735 747 default=True,
736 748 )
749 # If devel.debug.abort-update is True, then any merge with the working copy,
750 # e.g. [hg update], will be aborted after figuring out what needs to be done,
751 # but before spawning the parallel worker
752 coreconfigitem(
753 b'devel',
754 b'debug.abort-update',
755 default=False,
756 )
737 757 # If discovery.grow-sample is False, the sample size used in set discovery will
738 758 # not be increased through the process
739 759 coreconfigitem(
@@ -911,6 +931,13 b' coreconfigitem('
911 931 b'changegroup4',
912 932 default=False,
913 933 )
934
935 # might remove rank configuration once the computation has no impact
936 coreconfigitem(
937 b'experimental',
938 b'changelog-v2.compute-rank',
939 default=True,
940 )
914 941 coreconfigitem(
915 942 b'experimental',
916 943 b'cleanup-as-archived',
@@ -1774,6 +1801,13 b' coreconfigitem('
1774 1801 )
1775 1802 coreconfigitem(
1776 1803 b'merge-tools',
1804 br'.*\.regappend$',
1805 default=b"",
1806 generic=True,
1807 priority=-1,
1808 )
1809 coreconfigitem(
1810 b'merge-tools',
1777 1811 br'.*\.symlink$',
1778 1812 default=False,
1779 1813 generic=True,
@@ -2023,6 +2057,11 b' coreconfigitem('
2023 2057 )
2024 2058 coreconfigitem(
2025 2059 b'storage',
2060 b'revlog.delta-parent-search.candidate-group-chunk-size',
2061 default=10,
2062 )
2063 coreconfigitem(
2064 b'storage',
2026 2065 b'revlog.issue6528.fix-incoming',
2027 2066 default=True,
2028 2067 )
@@ -2044,6 +2083,7 b' coreconfigitem('
2044 2083 b'revlog.reuse-external-delta',
2045 2084 default=True,
2046 2085 )
2086 # This option is True unless `format.generaldelta` is set.
2047 2087 coreconfigitem(
2048 2088 b'storage',
2049 2089 b'revlog.reuse-external-delta-parent',
@@ -2123,7 +2163,7 b' coreconfigitem('
2123 2163 coreconfigitem(
2124 2164 b'server',
2125 2165 b'pullbundle',
2126 default=False,
2166 default=True,
2127 2167 )
2128 2168 coreconfigitem(
2129 2169 b'server',
@@ -1595,7 +1595,7 b' class workingctx(committablectx):'
1595 1595 if p2node is None:
1596 1596 p2node = self._repo.nodeconstants.nullid
1597 1597 dirstate = self._repo.dirstate
1598 with dirstate.parentchange():
1598 with dirstate.changing_parents(self._repo):
1599 1599 copies = dirstate.setparents(p1node, p2node)
1600 1600 pctx = self._repo[p1node]
1601 1601 if copies:
@@ -1854,47 +1854,42 b' class workingctx(committablectx):'
1854 1854
1855 1855 def _poststatusfixup(self, status, fixup):
1856 1856 """update dirstate for files that are actually clean"""
1857 dirstate = self._repo.dirstate
1857 1858 poststatus = self._repo.postdsstatus()
1858 if fixup or poststatus or self._repo.dirstate._dirty:
1859 if fixup:
1860 if dirstate.is_changing_parents:
1861 normal = lambda f, pfd: dirstate.update_file(
1862 f,
1863 p1_tracked=True,
1864 wc_tracked=True,
1865 )
1866 else:
1867 normal = dirstate.set_clean
1868 for f, pdf in fixup:
1869 normal(f, pdf)
1870 if poststatus or self._repo.dirstate._dirty:
1859 1871 try:
1860 oldid = self._repo.dirstate.identity()
1861
1862 1872 # updating the dirstate is optional
1863 1873 # so we don't wait on the lock
1864 1874 # wlock can invalidate the dirstate, so cache normal _after_
1865 1875 # taking the lock
1876 pre_dirty = dirstate._dirty
1866 1877 with self._repo.wlock(False):
1867 dirstate = self._repo.dirstate
1868 if dirstate.identity() == oldid:
1869 if fixup:
1870 if dirstate.pendingparentchange():
1871 normal = lambda f, pfd: dirstate.update_file(
1872 f, p1_tracked=True, wc_tracked=True
1873 )
1874 else:
1875 normal = dirstate.set_clean
1876 for f, pdf in fixup:
1877 normal(f, pdf)
1878 # write changes out explicitly, because nesting
1879 # wlock at runtime may prevent 'wlock.release()'
1880 # after this block from doing so for subsequent
1881 # changing files
1882 tr = self._repo.currenttransaction()
1883 self._repo.dirstate.write(tr)
1884
1885 if poststatus:
1886 for ps in poststatus:
1887 ps(self, status)
1888 else:
1889 # in this case, writing changes out breaks
1890 # consistency, because .hg/dirstate was
1891 # already changed simultaneously after last
1892 # caching (see also issue5584 for detail)
1893 self._repo.ui.debug(
1894 b'skip updating dirstate: identity mismatch\n'
1895 )
1878 assert self._repo.dirstate is dirstate
1879 post_dirty = dirstate._dirty
1880 if post_dirty:
1881 tr = self._repo.currenttransaction()
1882 dirstate.write(tr)
1883 elif pre_dirty:
1884 # the wlock grabbing detected that dirtate changes
1885 # needed to be dropped
1886 m = b'skip updating dirstate: identity mismatch\n'
1887 self._repo.ui.debug(m)
1888 if poststatus:
1889 for ps in poststatus:
1890 ps(self, status)
1896 1891 except error.LockError:
1897 pass
1892 dirstate.invalidate()
1898 1893 finally:
1899 1894 # Even if the wlock couldn't be grabbed, clear out the list.
1900 1895 self._repo.clearpostdsstatus()
@@ -1904,25 +1899,27 b' class workingctx(committablectx):'
1904 1899 subrepos = []
1905 1900 if b'.hgsub' in self:
1906 1901 subrepos = sorted(self.substate)
1907 cmp, s, mtime_boundary = self._repo.dirstate.status(
1908 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1909 )
1910
1911 # check for any possibly clean files
1912 fixup = []
1913 if cmp:
1914 modified2, deleted2, clean_set, fixup = self._checklookup(
1915 cmp, mtime_boundary
1902 dirstate = self._repo.dirstate
1903 with dirstate.running_status(self._repo):
1904 cmp, s, mtime_boundary = dirstate.status(
1905 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1916 1906 )
1917 s.modified.extend(modified2)
1918 s.deleted.extend(deleted2)
1919
1920 if clean_set and clean:
1921 s.clean.extend(clean_set)
1922 if fixup and clean:
1923 s.clean.extend((f for f, _ in fixup))
1924
1925 self._poststatusfixup(s, fixup)
1907
1908 # check for any possibly clean files
1909 fixup = []
1910 if cmp:
1911 modified2, deleted2, clean_set, fixup = self._checklookup(
1912 cmp, mtime_boundary
1913 )
1914 s.modified.extend(modified2)
1915 s.deleted.extend(deleted2)
1916
1917 if clean_set and clean:
1918 s.clean.extend(clean_set)
1919 if fixup and clean:
1920 s.clean.extend((f for f, _ in fixup))
1921
1922 self._poststatusfixup(s, fixup)
1926 1923
1927 1924 if match.always():
1928 1925 # cache for performance
@@ -2050,7 +2047,7 b' class workingctx(committablectx):'
2050 2047 return sorted(f for f in ds.matches(match) if ds.get_entry(f).tracked)
2051 2048
2052 2049 def markcommitted(self, node):
2053 with self._repo.dirstate.parentchange():
2050 with self._repo.dirstate.changing_parents(self._repo):
2054 2051 for f in self.modified() + self.added():
2055 2052 self._repo.dirstate.update_file(
2056 2053 f, p1_tracked=True, wc_tracked=True
This diff has been collapsed as it changes many lines, (526 lines changed) Show them Hide them
@@ -21,7 +21,6 b' import re'
21 21 import socket
22 22 import ssl
23 23 import stat
24 import string
25 24 import subprocess
26 25 import sys
27 26 import time
@@ -73,7 +72,6 b' from . import ('
73 72 repoview,
74 73 requirements,
75 74 revlog,
76 revlogutils,
77 75 revset,
78 76 revsetlang,
79 77 scmutil,
@@ -89,6 +87,7 b' from . import ('
89 87 upgrade,
90 88 url as urlmod,
91 89 util,
90 verify,
92 91 vfs as vfsmod,
93 92 wireprotoframing,
94 93 wireprotoserver,
@@ -556,15 +555,9 b' def debugchangedfiles(ui, repo, rev, **o'
556 555 @command(b'debugcheckstate', [], b'')
557 556 def debugcheckstate(ui, repo):
558 557 """validate the correctness of the current dirstate"""
559 parent1, parent2 = repo.dirstate.parents()
560 m1 = repo[parent1].manifest()
561 m2 = repo[parent2].manifest()
562 errors = 0
563 for err in repo.dirstate.verify(m1, m2):
564 ui.warn(err[0] % err[1:])
565 errors += 1
558 errors = verify.verifier(repo)._verify_dirstate()
566 559 if errors:
567 errstr = _(b".hg/dirstate inconsistent with current parent's manifest")
560 errstr = _(b"dirstate inconsistent with current parent's manifest")
568 561 raise error.Abort(errstr)
569 562
570 563
@@ -990,17 +983,29 b' def debugdeltachain(ui, repo, file_=None'
990 983
991 984 @command(
992 985 b'debug-delta-find',
993 cmdutil.debugrevlogopts + cmdutil.formatteropts,
986 cmdutil.debugrevlogopts
987 + cmdutil.formatteropts
988 + [
989 (
990 b'',
991 b'source',
992 b'full',
993 _(b'input data feed to the process (full, storage, p1, p2, prev)'),
994 ),
995 ],
994 996 _(b'-c|-m|FILE REV'),
995 997 optionalrepo=True,
996 998 )
997 def debugdeltafind(ui, repo, arg_1, arg_2=None, **opts):
999 def debugdeltafind(ui, repo, arg_1, arg_2=None, source=b'full', **opts):
998 1000 """display the computation to get to a valid delta for storing REV
999 1001
1000 1002 This command will replay the process used to find the "best" delta to store
1001 1003 a revision and display information about all the steps used to get to that
1002 1004 result.
1003 1005
1006 By default, the process is fed with a the full-text for the revision. This
1007 can be controlled with the --source flag.
1008
1004 1009 The revision use the revision number of the target storage (not changelog
1005 1010 revision number).
1006 1011
@@ -1017,34 +1022,22 b' def debugdeltafind(ui, repo, arg_1, arg_'
1017 1022 rev = int(rev)
1018 1023
1019 1024 revlog = cmdutil.openrevlog(repo, b'debugdeltachain', file_, opts)
1020
1021 deltacomputer = deltautil.deltacomputer(
1022 revlog,
1023 write_debug=ui.write,
1024 debug_search=not ui.quiet,
1025 )
1026
1027 node = revlog.node(rev)
1028 1025 p1r, p2r = revlog.parentrevs(rev)
1029 p1 = revlog.node(p1r)
1030 p2 = revlog.node(p2r)
1031 btext = [revlog.revision(rev)]
1032 textlen = len(btext[0])
1033 cachedelta = None
1034 flags = revlog.flags(rev)
1035
1036 revinfo = revlogutils.revisioninfo(
1037 node,
1038 p1,
1039 p2,
1040 btext,
1041 textlen,
1042 cachedelta,
1043 flags,
1044 )
1045
1046 fh = revlog._datafp()
1047 deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev)
1026
1027 if source == b'full':
1028 base_rev = nullrev
1029 elif source == b'storage':
1030 base_rev = revlog.deltaparent(rev)
1031 elif source == b'p1':
1032 base_rev = p1r
1033 elif source == b'p2':
1034 base_rev = p2r
1035 elif source == b'prev':
1036 base_rev = rev - 1
1037 else:
1038 raise error.InputError(b"invalid --source value: %s" % source)
1039
1040 revlog_debug.debug_delta_find(ui, revlog, rev, base_rev=base_rev)
1048 1041
1049 1042
1050 1043 @command(
@@ -1236,12 +1229,12 b' def debugdiscovery(ui, repo, remoteurl=b'
1236 1229 random.seed(int(opts[b'seed']))
1237 1230
1238 1231 if not remote_revs:
1239
1240 remoteurl, branches = urlutil.get_unique_pull_path(
1241 b'debugdiscovery', repo, ui, remoteurl
1232 path = urlutil.get_unique_pull_path_obj(
1233 b'debugdiscovery', ui, remoteurl
1242 1234 )
1243 remote = hg.peer(repo, opts, remoteurl)
1244 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(remoteurl))
1235 branches = (path.branch, [])
1236 remote = hg.peer(repo, opts, path)
1237 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(path.loc))
1245 1238 else:
1246 1239 branches = (None, [])
1247 1240 remote_filtered_revs = logcmdutil.revrange(
@@ -3135,6 +3128,9 b' def debugrebuilddirstate(ui, repo, rev, '
3135 3128 """
3136 3129 ctx = scmutil.revsingle(repo, rev)
3137 3130 with repo.wlock():
3131 if repo.currenttransaction() is not None:
3132 msg = b'rebuild the dirstate outside of a transaction'
3133 raise error.ProgrammingError(msg)
3138 3134 dirstate = repo.dirstate
3139 3135 changedfiles = None
3140 3136 # See command doc for what minimal does.
@@ -3146,7 +3142,8 b' def debugrebuilddirstate(ui, repo, rev, '
3146 3142 dsnotadded = {f for f in dsonly if not dirstate.get_entry(f).added}
3147 3143 changedfiles = manifestonly | dsnotadded
3148 3144
3149 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
3145 with dirstate.changing_parents(repo):
3146 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
3150 3147
3151 3148
3152 3149 @command(
@@ -3207,348 +3204,10 b' def debugrevlog(ui, repo, file_=None, **'
3207 3204 r = cmdutil.openrevlog(repo, b'debugrevlog', file_, opts)
3208 3205
3209 3206 if opts.get(b"dump"):
3210 numrevs = len(r)
3211 ui.write(
3212 (
3213 b"# rev p1rev p2rev start end deltastart base p1 p2"
3214 b" rawsize totalsize compression heads chainlen\n"
3215 )
3216 )
3217 ts = 0
3218 heads = set()
3219
3220 for rev in range(numrevs):
3221 dbase = r.deltaparent(rev)
3222 if dbase == -1:
3223 dbase = rev
3224 cbase = r.chainbase(rev)
3225 clen = r.chainlen(rev)
3226 p1, p2 = r.parentrevs(rev)
3227 rs = r.rawsize(rev)
3228 ts = ts + rs
3229 heads -= set(r.parentrevs(rev))
3230 heads.add(rev)
3231 try:
3232 compression = ts / r.end(rev)
3233 except ZeroDivisionError:
3234 compression = 0
3235 ui.write(
3236 b"%5d %5d %5d %5d %5d %10d %4d %4d %4d %7d %9d "
3237 b"%11d %5d %8d\n"
3238 % (
3239 rev,
3240 p1,
3241 p2,
3242 r.start(rev),
3243 r.end(rev),
3244 r.start(dbase),
3245 r.start(cbase),
3246 r.start(p1),
3247 r.start(p2),
3248 rs,
3249 ts,
3250 compression,
3251 len(heads),
3252 clen,
3253 )
3254 )
3255 return 0
3256
3257 format = r._format_version
3258 v = r._format_flags
3259 flags = []
3260 gdelta = False
3261 if v & revlog.FLAG_INLINE_DATA:
3262 flags.append(b'inline')
3263 if v & revlog.FLAG_GENERALDELTA:
3264 gdelta = True
3265 flags.append(b'generaldelta')
3266 if not flags:
3267 flags = [b'(none)']
3268
3269 ### tracks merge vs single parent
3270 nummerges = 0
3271
3272 ### tracks ways the "delta" are build
3273 # nodelta
3274 numempty = 0
3275 numemptytext = 0
3276 numemptydelta = 0
3277 # full file content
3278 numfull = 0
3279 # intermediate snapshot against a prior snapshot
3280 numsemi = 0
3281 # snapshot count per depth
3282 numsnapdepth = collections.defaultdict(lambda: 0)
3283 # delta against previous revision
3284 numprev = 0
3285 # delta against first or second parent (not prev)
3286 nump1 = 0
3287 nump2 = 0
3288 # delta against neither prev nor parents
3289 numother = 0
3290 # delta against prev that are also first or second parent
3291 # (details of `numprev`)
3292 nump1prev = 0
3293 nump2prev = 0
3294
3295 # data about delta chain of each revs
3296 chainlengths = []
3297 chainbases = []
3298 chainspans = []
3299
3300 # data about each revision
3301 datasize = [None, 0, 0]
3302 fullsize = [None, 0, 0]
3303 semisize = [None, 0, 0]
3304 # snapshot count per depth
3305 snapsizedepth = collections.defaultdict(lambda: [None, 0, 0])
3306 deltasize = [None, 0, 0]
3307 chunktypecounts = {}
3308 chunktypesizes = {}
3309
3310 def addsize(size, l):
3311 if l[0] is None or size < l[0]:
3312 l[0] = size
3313 if size > l[1]:
3314 l[1] = size
3315 l[2] += size
3316
3317 numrevs = len(r)
3318 for rev in range(numrevs):
3319 p1, p2 = r.parentrevs(rev)
3320 delta = r.deltaparent(rev)
3321 if format > 0:
3322 addsize(r.rawsize(rev), datasize)
3323 if p2 != nullrev:
3324 nummerges += 1
3325 size = r.length(rev)
3326 if delta == nullrev:
3327 chainlengths.append(0)
3328 chainbases.append(r.start(rev))
3329 chainspans.append(size)
3330 if size == 0:
3331 numempty += 1
3332 numemptytext += 1
3333 else:
3334 numfull += 1
3335 numsnapdepth[0] += 1
3336 addsize(size, fullsize)
3337 addsize(size, snapsizedepth[0])
3338 else:
3339 chainlengths.append(chainlengths[delta] + 1)
3340 baseaddr = chainbases[delta]
3341 revaddr = r.start(rev)
3342 chainbases.append(baseaddr)
3343 chainspans.append((revaddr - baseaddr) + size)
3344 if size == 0:
3345 numempty += 1
3346 numemptydelta += 1
3347 elif r.issnapshot(rev):
3348 addsize(size, semisize)
3349 numsemi += 1
3350 depth = r.snapshotdepth(rev)
3351 numsnapdepth[depth] += 1
3352 addsize(size, snapsizedepth[depth])
3353 else:
3354 addsize(size, deltasize)
3355 if delta == rev - 1:
3356 numprev += 1
3357 if delta == p1:
3358 nump1prev += 1
3359 elif delta == p2:
3360 nump2prev += 1
3361 elif delta == p1:
3362 nump1 += 1
3363 elif delta == p2:
3364 nump2 += 1
3365 elif delta != nullrev:
3366 numother += 1
3367
3368 # Obtain data on the raw chunks in the revlog.
3369 if util.safehasattr(r, b'_getsegmentforrevs'):
3370 segment = r._getsegmentforrevs(rev, rev)[1]
3371 else:
3372 segment = r._revlog._getsegmentforrevs(rev, rev)[1]
3373 if segment:
3374 chunktype = bytes(segment[0:1])
3375 else:
3376 chunktype = b'empty'
3377
3378 if chunktype not in chunktypecounts:
3379 chunktypecounts[chunktype] = 0
3380 chunktypesizes[chunktype] = 0
3381
3382 chunktypecounts[chunktype] += 1
3383 chunktypesizes[chunktype] += size
3384
3385 # Adjust size min value for empty cases
3386 for size in (datasize, fullsize, semisize, deltasize):
3387 if size[0] is None:
3388 size[0] = 0
3389
3390 numdeltas = numrevs - numfull - numempty - numsemi
3391 numoprev = numprev - nump1prev - nump2prev
3392 totalrawsize = datasize[2]
3393 datasize[2] /= numrevs
3394 fulltotal = fullsize[2]
3395 if numfull == 0:
3396 fullsize[2] = 0
3207 revlog_debug.dump(ui, r)
3397 3208 else:
3398 fullsize[2] /= numfull
3399 semitotal = semisize[2]
3400 snaptotal = {}
3401 if numsemi > 0:
3402 semisize[2] /= numsemi
3403 for depth in snapsizedepth:
3404 snaptotal[depth] = snapsizedepth[depth][2]
3405 snapsizedepth[depth][2] /= numsnapdepth[depth]
3406
3407 deltatotal = deltasize[2]
3408 if numdeltas > 0:
3409 deltasize[2] /= numdeltas
3410 totalsize = fulltotal + semitotal + deltatotal
3411 avgchainlen = sum(chainlengths) / numrevs
3412 maxchainlen = max(chainlengths)
3413 maxchainspan = max(chainspans)
3414 compratio = 1
3415 if totalsize:
3416 compratio = totalrawsize / totalsize
3417
3418 basedfmtstr = b'%%%dd\n'
3419 basepcfmtstr = b'%%%dd %s(%%5.2f%%%%)\n'
3420
3421 def dfmtstr(max):
3422 return basedfmtstr % len(str(max))
3423
3424 def pcfmtstr(max, padding=0):
3425 return basepcfmtstr % (len(str(max)), b' ' * padding)
3426
3427 def pcfmt(value, total):
3428 if total:
3429 return (value, 100 * float(value) / total)
3430 else:
3431 return value, 100.0
3432
3433 ui.writenoi18n(b'format : %d\n' % format)
3434 ui.writenoi18n(b'flags : %s\n' % b', '.join(flags))
3435
3436 ui.write(b'\n')
3437 fmt = pcfmtstr(totalsize)
3438 fmt2 = dfmtstr(totalsize)
3439 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
3440 ui.writenoi18n(b' merges : ' + fmt % pcfmt(nummerges, numrevs))
3441 ui.writenoi18n(
3442 b' normal : ' + fmt % pcfmt(numrevs - nummerges, numrevs)
3443 )
3444 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
3445 ui.writenoi18n(b' empty : ' + fmt % pcfmt(numempty, numrevs))
3446 ui.writenoi18n(
3447 b' text : '
3448 + fmt % pcfmt(numemptytext, numemptytext + numemptydelta)
3449 )
3450 ui.writenoi18n(
3451 b' delta : '
3452 + fmt % pcfmt(numemptydelta, numemptytext + numemptydelta)
3453 )
3454 ui.writenoi18n(
3455 b' snapshot : ' + fmt % pcfmt(numfull + numsemi, numrevs)
3456 )
3457 for depth in sorted(numsnapdepth):
3458 ui.write(
3459 (b' lvl-%-3d : ' % depth)
3460 + fmt % pcfmt(numsnapdepth[depth], numrevs)
3461 )
3462 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(numdeltas, numrevs))
3463 ui.writenoi18n(b'revision size : ' + fmt2 % totalsize)
3464 ui.writenoi18n(
3465 b' snapshot : ' + fmt % pcfmt(fulltotal + semitotal, totalsize)
3466 )
3467 for depth in sorted(numsnapdepth):
3468 ui.write(
3469 (b' lvl-%-3d : ' % depth)
3470 + fmt % pcfmt(snaptotal[depth], totalsize)
3471 )
3472 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(deltatotal, totalsize))
3473
3474 def fmtchunktype(chunktype):
3475 if chunktype == b'empty':
3476 return b' %s : ' % chunktype
3477 elif chunktype in pycompat.bytestr(string.ascii_letters):
3478 return b' 0x%s (%s) : ' % (hex(chunktype), chunktype)
3479 else:
3480 return b' 0x%s : ' % hex(chunktype)
3481
3482 ui.write(b'\n')
3483 ui.writenoi18n(b'chunks : ' + fmt2 % numrevs)
3484 for chunktype in sorted(chunktypecounts):
3485 ui.write(fmtchunktype(chunktype))
3486 ui.write(fmt % pcfmt(chunktypecounts[chunktype], numrevs))
3487 ui.writenoi18n(b'chunks size : ' + fmt2 % totalsize)
3488 for chunktype in sorted(chunktypecounts):
3489 ui.write(fmtchunktype(chunktype))
3490 ui.write(fmt % pcfmt(chunktypesizes[chunktype], totalsize))
3491
3492 ui.write(b'\n')
3493 fmt = dfmtstr(max(avgchainlen, maxchainlen, maxchainspan, compratio))
3494 ui.writenoi18n(b'avg chain length : ' + fmt % avgchainlen)
3495 ui.writenoi18n(b'max chain length : ' + fmt % maxchainlen)
3496 ui.writenoi18n(b'max chain reach : ' + fmt % maxchainspan)
3497 ui.writenoi18n(b'compression ratio : ' + fmt % compratio)
3498
3499 if format > 0:
3500 ui.write(b'\n')
3501 ui.writenoi18n(
3502 b'uncompressed data size (min/max/avg) : %d / %d / %d\n'
3503 % tuple(datasize)
3504 )
3505 ui.writenoi18n(
3506 b'full revision size (min/max/avg) : %d / %d / %d\n'
3507 % tuple(fullsize)
3508 )
3509 ui.writenoi18n(
3510 b'inter-snapshot size (min/max/avg) : %d / %d / %d\n'
3511 % tuple(semisize)
3512 )
3513 for depth in sorted(snapsizedepth):
3514 if depth == 0:
3515 continue
3516 ui.writenoi18n(
3517 b' level-%-3d (min/max/avg) : %d / %d / %d\n'
3518 % ((depth,) + tuple(snapsizedepth[depth]))
3519 )
3520 ui.writenoi18n(
3521 b'delta size (min/max/avg) : %d / %d / %d\n'
3522 % tuple(deltasize)
3523 )
3524
3525 if numdeltas > 0:
3526 ui.write(b'\n')
3527 fmt = pcfmtstr(numdeltas)
3528 fmt2 = pcfmtstr(numdeltas, 4)
3529 ui.writenoi18n(
3530 b'deltas against prev : ' + fmt % pcfmt(numprev, numdeltas)
3531 )
3532 if numprev > 0:
3533 ui.writenoi18n(
3534 b' where prev = p1 : ' + fmt2 % pcfmt(nump1prev, numprev)
3535 )
3536 ui.writenoi18n(
3537 b' where prev = p2 : ' + fmt2 % pcfmt(nump2prev, numprev)
3538 )
3539 ui.writenoi18n(
3540 b' other : ' + fmt2 % pcfmt(numoprev, numprev)
3541 )
3542 if gdelta:
3543 ui.writenoi18n(
3544 b'deltas against p1 : ' + fmt % pcfmt(nump1, numdeltas)
3545 )
3546 ui.writenoi18n(
3547 b'deltas against p2 : ' + fmt % pcfmt(nump2, numdeltas)
3548 )
3549 ui.writenoi18n(
3550 b'deltas against other : ' + fmt % pcfmt(numother, numdeltas)
3551 )
3209 revlog_debug.debug_revlog(ui, r)
3210 return 0
3552 3211
3553 3212
3554 3213 @command(
@@ -3935,10 +3594,8 b' def debugssl(ui, repo, source=None, **op'
3935 3594 )
3936 3595 source = b"default"
3937 3596
3938 source, branches = urlutil.get_unique_pull_path(
3939 b'debugssl', repo, ui, source
3940 )
3941 url = urlutil.url(source)
3597 path = urlutil.get_unique_pull_path_obj(b'debugssl', ui, source)
3598 url = path.url
3942 3599
3943 3600 defaultport = {b'https': 443, b'ssh': 22}
3944 3601 if url.scheme in defaultport:
@@ -4049,20 +3706,19 b' def debugbackupbundle(ui, repo, *pats, *'
4049 3706 for backup in backups:
4050 3707 # Much of this is copied from the hg incoming logic
4051 3708 source = os.path.relpath(backup, encoding.getcwd())
4052 source, branches = urlutil.get_unique_pull_path(
3709 path = urlutil.get_unique_pull_path_obj(
4053 3710 b'debugbackupbundle',
4054 repo,
4055 3711 ui,
4056 3712 source,
4057 default_branches=opts.get(b'branch'),
4058 3713 )
4059 3714 try:
4060 other = hg.peer(repo, opts, source)
3715 other = hg.peer(repo, opts, path)
4061 3716 except error.LookupError as ex:
4062 msg = _(b"\nwarning: unable to open bundle %s") % source
3717 msg = _(b"\nwarning: unable to open bundle %s") % path.loc
4063 3718 hint = _(b"\n(missing parent rev %s)\n") % short(ex.name)
4064 3719 ui.warn(msg, hint=hint)
4065 3720 continue
3721 branches = (path.branch, opts.get(b'branch', []))
4066 3722 revs, checkout = hg.addbranchrevs(
4067 3723 repo, other, branches, opts.get(b"rev")
4068 3724 )
@@ -4085,29 +3741,29 b' def debugbackupbundle(ui, repo, *pats, *'
4085 3741 with repo.lock(), repo.transaction(b"unbundle") as tr:
4086 3742 if scmutil.isrevsymbol(other, recovernode):
4087 3743 ui.status(_(b"Unbundling %s\n") % (recovernode))
4088 f = hg.openpath(ui, source)
4089 gen = exchange.readbundle(ui, f, source)
3744 f = hg.openpath(ui, path.loc)
3745 gen = exchange.readbundle(ui, f, path.loc)
4090 3746 if isinstance(gen, bundle2.unbundle20):
4091 3747 bundle2.applybundle(
4092 3748 repo,
4093 3749 gen,
4094 3750 tr,
4095 3751 source=b"unbundle",
4096 url=b"bundle:" + source,
3752 url=b"bundle:" + path.loc,
4097 3753 )
4098 3754 else:
4099 gen.apply(repo, b"unbundle", b"bundle:" + source)
3755 gen.apply(repo, b"unbundle", b"bundle:" + path.loc)
4100 3756 break
4101 3757 else:
4102 3758 backupdate = encoding.strtolocal(
4103 3759 time.strftime(
4104 3760 "%a %H:%M, %Y-%m-%d",
4105 time.localtime(os.path.getmtime(source)),
3761 time.localtime(os.path.getmtime(path.loc)),
4106 3762 )
4107 3763 )
4108 3764 ui.status(b"\n%s\n" % (backupdate.ljust(50)))
4109 3765 if ui.verbose:
4110 ui.status(b"%s%s\n" % (b"bundle:".ljust(13), source))
3766 ui.status(b"%s%s\n" % (b"bundle:".ljust(13), path.loc))
4111 3767 else:
4112 3768 opts[
4113 3769 b"template"
@@ -4134,8 +3790,21 b' def debugsub(ui, repo, rev=None):'
4134 3790 ui.writenoi18n(b' revision %s\n' % v[1])
4135 3791
4136 3792
4137 @command(b'debugshell', optionalrepo=True)
4138 def debugshell(ui, repo):
3793 @command(
3794 b'debugshell',
3795 [
3796 (
3797 b'c',
3798 b'command',
3799 b'',
3800 _(b'program passed in as a string'),
3801 _(b'COMMAND'),
3802 )
3803 ],
3804 _(b'[-c COMMAND]'),
3805 optionalrepo=True,
3806 )
3807 def debugshell(ui, repo, **opts):
4139 3808 """run an interactive Python interpreter
4140 3809
4141 3810 The local namespace is provided with a reference to the ui and
@@ -4148,10 +3817,58 b' def debugshell(ui, repo):'
4148 3817 'repo': repo,
4149 3818 }
4150 3819
3820 # py2exe disables initialization of the site module, which is responsible
3821 # for arranging for ``quit()`` to exit the interpreter. Manually initialize
3822 # the stuff that site normally does here, so that the interpreter can be
3823 # quit in a consistent manner, whether run with pyoxidizer, exewrapper.c,
3824 # py.exe, or py2exe.
3825 if getattr(sys, "frozen", None) == 'console_exe':
3826 try:
3827 import site
3828
3829 site.setcopyright()
3830 site.sethelper()
3831 site.setquit()
3832 except ImportError:
3833 site = None # Keep PyCharm happy
3834
3835 command = opts.get('command')
3836 if command:
3837 compiled = code.compile_command(encoding.strfromlocal(command))
3838 code.InteractiveInterpreter(locals=imported_objects).runcode(compiled)
3839 return
3840
4151 3841 code.interact(local=imported_objects)
4152 3842
4153 3843
4154 3844 @command(
3845 b'debug-revlog-stats',
3846 [
3847 (b'c', b'changelog', None, _(b'Display changelog statistics')),
3848 (b'm', b'manifest', None, _(b'Display manifest statistics')),
3849 (b'f', b'filelogs', None, _(b'Display filelogs statistics')),
3850 ]
3851 + cmdutil.formatteropts,
3852 )
3853 def debug_revlog_stats(ui, repo, **opts):
3854 """display statistics about revlogs in the store"""
3855 opts = pycompat.byteskwargs(opts)
3856 changelog = opts[b"changelog"]
3857 manifest = opts[b"manifest"]
3858 filelogs = opts[b"filelogs"]
3859
3860 if changelog is None and manifest is None and filelogs is None:
3861 changelog = True
3862 manifest = True
3863 filelogs = True
3864
3865 repo = repo.unfiltered()
3866 fm = ui.formatter(b'debug-revlog-stats', opts)
3867 revlog_debug.debug_revlog_stats(repo, fm, changelog, manifest, filelogs)
3868 fm.end()
3869
3870
3871 @command(
4155 3872 b'debugsuccessorssets',
4156 3873 [(b'', b'closest', False, _(b'return closest successors sets only'))],
4157 3874 _(b'[REV]'),
@@ -4843,7 +4560,8 b' def debugwireproto(ui, repo, path=None, '
4843 4560 _(b'--peer %s not supported with HTTP peers') % opts[b'peer']
4844 4561 )
4845 4562 else:
4846 peer = httppeer.makepeer(ui, path, opener=opener)
4563 peer_path = urlutil.try_path(ui, path)
4564 peer = httppeer.makepeer(ui, peer_path, opener=opener)
4847 4565
4848 4566 # We /could/ populate stdin/stdout with sock.makefile()...
4849 4567 else:
@@ -120,7 +120,7 b' def difffeatureopts('
120 120 )
121 121 buildopts[b'ignorewseol'] = get(b'ignore_space_at_eol', b'ignorewseol')
122 122 if formatchanging:
123 buildopts[b'text'] = opts and opts.get(b'text')
123 buildopts[b'text'] = None if opts is None else opts.get(b'text')
124 124 binary = None if opts is None else opts.get(b'binary')
125 125 buildopts[b'nobinary'] = (
126 126 not binary
This diff has been collapsed as it changes many lines, (557 lines changed) Show them Hide them
@@ -31,7 +31,6 b' from . import ('
31 31 )
32 32
33 33 from .dirstateutils import (
34 docket as docketmod,
35 34 timestamp,
36 35 )
37 36
@@ -66,10 +65,17 b' class rootcache(filecache):'
66 65 return obj._join(fname)
67 66
68 67
69 def requires_parents_change(func):
68 def check_invalidated(func):
69 """check that the func is called with a non-invalidated dirstate
70
71 The dirstate is in an "invalidated state" after an error occured during its
72 modification and remains so until we exited the top level scope that framed
73 such change.
74 """
75
70 76 def wrap(self, *args, **kwargs):
71 if not self.pendingparentchange():
72 msg = 'calling `%s` outside of a parentchange context'
77 if self._invalidated_context:
78 msg = 'calling `%s` after the dirstate was invalidated'
73 79 msg %= func.__name__
74 80 raise error.ProgrammingError(msg)
75 81 return func(self, *args, **kwargs)
@@ -77,19 +83,63 b' def requires_parents_change(func):'
77 83 return wrap
78 84
79 85
80 def requires_no_parents_change(func):
86 def requires_changing_parents(func):
81 87 def wrap(self, *args, **kwargs):
82 if self.pendingparentchange():
83 msg = 'calling `%s` inside of a parentchange context'
88 if not self.is_changing_parents:
89 msg = 'calling `%s` outside of a changing_parents context'
90 msg %= func.__name__
91 raise error.ProgrammingError(msg)
92 return func(self, *args, **kwargs)
93
94 return check_invalidated(wrap)
95
96
97 def requires_changing_files(func):
98 def wrap(self, *args, **kwargs):
99 if not self.is_changing_files:
100 msg = 'calling `%s` outside of a `changing_files`'
84 101 msg %= func.__name__
85 102 raise error.ProgrammingError(msg)
86 103 return func(self, *args, **kwargs)
87 104
88 return wrap
105 return check_invalidated(wrap)
106
107
108 def requires_changing_any(func):
109 def wrap(self, *args, **kwargs):
110 if not self.is_changing_any:
111 msg = 'calling `%s` outside of a changing context'
112 msg %= func.__name__
113 raise error.ProgrammingError(msg)
114 return func(self, *args, **kwargs)
115
116 return check_invalidated(wrap)
117
118
119 def requires_changing_files_or_status(func):
120 def wrap(self, *args, **kwargs):
121 if not (self.is_changing_files or self._running_status > 0):
122 msg = (
123 'calling `%s` outside of a changing_files '
124 'or running_status context'
125 )
126 msg %= func.__name__
127 raise error.ProgrammingError(msg)
128 return func(self, *args, **kwargs)
129
130 return check_invalidated(wrap)
131
132
133 CHANGE_TYPE_PARENTS = "parents"
134 CHANGE_TYPE_FILES = "files"
89 135
90 136
91 137 @interfaceutil.implementer(intdirstate.idirstate)
92 138 class dirstate:
139
140 # used by largefile to avoid overwritting transaction callback
141 _tr_key_suffix = b''
142
93 143 def __init__(
94 144 self,
95 145 opener,
@@ -124,7 +174,16 b' class dirstate:'
124 174 self._dirty_tracked_set = False
125 175 self._ui = ui
126 176 self._filecache = {}
127 self._parentwriters = 0
177 # nesting level of `changing_parents` context
178 self._changing_level = 0
179 # the change currently underway
180 self._change_type = None
181 # number of open _running_status context
182 self._running_status = 0
183 # True if the current dirstate changing operations have been
184 # invalidated (used to make sure all nested contexts have been exited)
185 self._invalidated_context = False
186 self._attached_to_a_transaction = False
128 187 self._filename = b'dirstate'
129 188 self._filename_th = b'dirstate-tracked-hint'
130 189 self._pendingfilename = b'%s.pending' % self._filename
@@ -136,6 +195,12 b' class dirstate:'
136 195 # raises an exception).
137 196 self._cwd
138 197
198 def refresh(self):
199 if '_branch' in vars(self):
200 del self._branch
201 if '_map' in vars(self) and self._map.may_need_refresh():
202 self.invalidate()
203
139 204 def prefetch_parents(self):
140 205 """make sure the parents are loaded
141 206
@@ -144,39 +209,193 b' class dirstate:'
144 209 self._pl
145 210
146 211 @contextlib.contextmanager
147 def parentchange(self):
148 """Context manager for handling dirstate parents.
212 @check_invalidated
213 def running_status(self, repo):
214 """Wrap a status operation
215
216 This context is not mutally exclusive with the `changing_*` context. It
217 also do not warrant for the `wlock` to be taken.
218
219 If the wlock is taken, this context will behave in a simple way, and
220 ensure the data are scheduled for write when leaving the top level
221 context.
149 222
150 If an exception occurs in the scope of the context manager,
151 the incoherent dirstate won't be written when wlock is
152 released.
223 If the lock is not taken, it will only warrant that the data are either
224 committed (written) and rolled back (invalidated) when exiting the top
225 level context. The write/invalidate action must be performed by the
226 wrapped code.
227
228
229 The expected logic is:
230
231 A: read the dirstate
232 B: run status
233 This might make the dirstate dirty by updating cache,
234 especially in Rust.
235 C: do more "post status fixup if relevant
236 D: try to take the w-lock (this will invalidate the changes if they were raced)
237 E0: if dirstate changed on disk → discard change (done by dirstate internal)
238 E1: elif lock was acquired → write the changes
239 E2: else → discard the changes
153 240 """
154 self._parentwriters += 1
155 yield
156 # Typically we want the "undo" step of a context manager in a
157 # finally block so it happens even when an exception
158 # occurs. In this case, however, we only want to decrement
159 # parentwriters if the code in the with statement exits
160 # normally, so we don't have a try/finally here on purpose.
161 self._parentwriters -= 1
241 has_lock = repo.currentwlock() is not None
242 is_changing = self.is_changing_any
243 tr = repo.currenttransaction()
244 has_tr = tr is not None
245 nested = bool(self._running_status)
246
247 first_and_alone = not (is_changing or has_tr or nested)
248
249 # enforce no change happened outside of a proper context.
250 if first_and_alone and self._dirty:
251 has_tr = repo.currenttransaction() is not None
252 if not has_tr and self._changing_level == 0 and self._dirty:
253 msg = "entering a status context, but dirstate is already dirty"
254 raise error.ProgrammingError(msg)
255
256 should_write = has_lock and not (nested or is_changing)
257
258 self._running_status += 1
259 try:
260 yield
261 except Exception:
262 self.invalidate()
263 raise
264 finally:
265 self._running_status -= 1
266 if self._invalidated_context:
267 should_write = False
268 self.invalidate()
269
270 if should_write:
271 assert repo.currenttransaction() is tr
272 self.write(tr)
273 elif not has_lock:
274 if self._dirty:
275 msg = b'dirstate dirty while exiting an isolated status context'
276 repo.ui.develwarn(msg)
277 self.invalidate()
278
279 @contextlib.contextmanager
280 @check_invalidated
281 def _changing(self, repo, change_type):
282 if repo.currentwlock() is None:
283 msg = b"trying to change the dirstate without holding the wlock"
284 raise error.ProgrammingError(msg)
285
286 has_tr = repo.currenttransaction() is not None
287 if not has_tr and self._changing_level == 0 and self._dirty:
288 msg = b"entering a changing context, but dirstate is already dirty"
289 repo.ui.develwarn(msg)
290
291 assert self._changing_level >= 0
292 # different type of change are mutually exclusive
293 if self._change_type is None:
294 assert self._changing_level == 0
295 self._change_type = change_type
296 elif self._change_type != change_type:
297 msg = (
298 'trying to open "%s" dirstate-changing context while a "%s" is'
299 ' already open'
300 )
301 msg %= (change_type, self._change_type)
302 raise error.ProgrammingError(msg)
303 should_write = False
304 self._changing_level += 1
305 try:
306 yield
307 except: # re-raises
308 self.invalidate() # this will set `_invalidated_context`
309 raise
310 finally:
311 assert self._changing_level > 0
312 self._changing_level -= 1
313 # If the dirstate is being invalidated, call invalidate again.
314 # This will throw away anything added by a upper context and
315 # reset the `_invalidated_context` flag when relevant
316 if self._changing_level <= 0:
317 self._change_type = None
318 assert self._changing_level == 0
319 if self._invalidated_context:
320 # make sure we invalidate anything an upper context might
321 # have changed.
322 self.invalidate()
323 else:
324 should_write = self._changing_level <= 0
325 tr = repo.currenttransaction()
326 if has_tr != (tr is not None):
327 if has_tr:
328 m = "transaction vanished while changing dirstate"
329 else:
330 m = "transaction appeared while changing dirstate"
331 raise error.ProgrammingError(m)
332 if should_write:
333 self.write(tr)
334
335 @contextlib.contextmanager
336 def changing_parents(self, repo):
337 with self._changing(repo, CHANGE_TYPE_PARENTS) as c:
338 yield c
339
340 @contextlib.contextmanager
341 def changing_files(self, repo):
342 with self._changing(repo, CHANGE_TYPE_FILES) as c:
343 yield c
344
345 # here to help migration to the new code
346 def parentchange(self):
347 msg = (
348 "Mercurial 6.4 and later requires call to "
349 "`dirstate.changing_parents(repo)`"
350 )
351 raise error.ProgrammingError(msg)
352
353 @property
354 def is_changing_any(self):
355 """Returns true if the dirstate is in the middle of a set of changes.
356
357 This returns True for any kind of change.
358 """
359 return self._changing_level > 0
162 360
163 361 def pendingparentchange(self):
362 return self.is_changing_parent()
363
364 def is_changing_parent(self):
164 365 """Returns true if the dirstate is in the middle of a set of changes
165 366 that modify the dirstate parent.
166 367 """
167 return self._parentwriters > 0
368 self._ui.deprecwarn(b"dirstate.is_changing_parents", b"6.5")
369 return self.is_changing_parents
370
371 @property
372 def is_changing_parents(self):
373 """Returns true if the dirstate is in the middle of a set of changes
374 that modify the dirstate parent.
375 """
376 if self._changing_level <= 0:
377 return False
378 return self._change_type == CHANGE_TYPE_PARENTS
379
380 @property
381 def is_changing_files(self):
382 """Returns true if the dirstate is in the middle of a set of changes
383 that modify the files tracked or their sources.
384 """
385 if self._changing_level <= 0:
386 return False
387 return self._change_type == CHANGE_TYPE_FILES
168 388
169 389 @propertycache
170 390 def _map(self):
171 391 """Return the dirstate contents (see documentation for dirstatemap)."""
172 self._map = self._mapcls(
392 return self._mapcls(
173 393 self._ui,
174 394 self._opener,
175 395 self._root,
176 396 self._nodeconstants,
177 397 self._use_dirstate_v2,
178 398 )
179 return self._map
180 399
181 400 @property
182 401 def _sparsematcher(self):
@@ -365,6 +584,7 b' class dirstate:'
365 584 def branch(self):
366 585 return encoding.tolocal(self._branch)
367 586
587 @requires_changing_parents
368 588 def setparents(self, p1, p2=None):
369 589 """Set dirstate parents to p1 and p2.
370 590
@@ -376,10 +596,10 b' class dirstate:'
376 596 """
377 597 if p2 is None:
378 598 p2 = self._nodeconstants.nullid
379 if self._parentwriters == 0:
599 if self._changing_level == 0:
380 600 raise ValueError(
381 601 b"cannot set dirstate parent outside of "
382 b"dirstate.parentchange context manager"
602 b"dirstate.changing_parents context manager"
383 603 )
384 604
385 605 self._dirty = True
@@ -419,9 +639,14 b' class dirstate:'
419 639 delattr(self, a)
420 640 self._dirty = False
421 641 self._dirty_tracked_set = False
422 self._parentwriters = 0
642 self._invalidated_context = bool(
643 self._changing_level > 0
644 or self._attached_to_a_transaction
645 or self._running_status
646 )
423 647 self._origpl = None
424 648
649 @requires_changing_any
425 650 def copy(self, source, dest):
426 651 """Mark dest as a copy of source. Unmark dest if source is None."""
427 652 if source == dest:
@@ -439,7 +664,7 b' class dirstate:'
439 664 def copies(self):
440 665 return self._map.copymap
441 666
442 @requires_no_parents_change
667 @requires_changing_files
443 668 def set_tracked(self, filename, reset_copy=False):
444 669 """a "public" method for generic code to mark a file as tracked
445 670
@@ -461,7 +686,7 b' class dirstate:'
461 686 self._dirty_tracked_set = True
462 687 return pre_tracked
463 688
464 @requires_no_parents_change
689 @requires_changing_files
465 690 def set_untracked(self, filename):
466 691 """a "public" method for generic code to mark a file as untracked
467 692
@@ -476,7 +701,7 b' class dirstate:'
476 701 self._dirty_tracked_set = True
477 702 return ret
478 703
479 @requires_no_parents_change
704 @requires_changing_files_or_status
480 705 def set_clean(self, filename, parentfiledata):
481 706 """record that the current state of the file on disk is known to be clean"""
482 707 self._dirty = True
@@ -485,13 +710,13 b' class dirstate:'
485 710 (mode, size, mtime) = parentfiledata
486 711 self._map.set_clean(filename, mode, size, mtime)
487 712
488 @requires_no_parents_change
713 @requires_changing_files_or_status
489 714 def set_possibly_dirty(self, filename):
490 715 """record that the current state of the file on disk is unknown"""
491 716 self._dirty = True
492 717 self._map.set_possibly_dirty(filename)
493 718
494 @requires_parents_change
719 @requires_changing_parents
495 720 def update_file_p1(
496 721 self,
497 722 filename,
@@ -503,7 +728,7 b' class dirstate:'
503 728 rewriting operation.
504 729
505 730 It should not be called during a merge (p2 != nullid) and only within
506 a `with dirstate.parentchange():` context.
731 a `with dirstate.changing_parents(repo):` context.
507 732 """
508 733 if self.in_merge:
509 734 msg = b'update_file_reference should not be called when merging'
@@ -531,7 +756,7 b' class dirstate:'
531 756 has_meaningful_mtime=False,
532 757 )
533 758
534 @requires_parents_change
759 @requires_changing_parents
535 760 def update_file(
536 761 self,
537 762 filename,
@@ -546,12 +771,57 b' class dirstate:'
546 771 This is to be called when the direstates parent changes to keep track
547 772 of what is the file situation in regards to the working copy and its parent.
548 773
549 This function must be called within a `dirstate.parentchange` context.
774 This function must be called within a `dirstate.changing_parents` context.
550 775
551 776 note: the API is at an early stage and we might need to adjust it
552 777 depending of what information ends up being relevant and useful to
553 778 other processing.
554 779 """
780 self._update_file(
781 filename=filename,
782 wc_tracked=wc_tracked,
783 p1_tracked=p1_tracked,
784 p2_info=p2_info,
785 possibly_dirty=possibly_dirty,
786 parentfiledata=parentfiledata,
787 )
788
789 def hacky_extension_update_file(self, *args, **kwargs):
790 """NEVER USE THIS, YOU DO NOT NEED IT
791
792 This function is a variant of "update_file" to be called by a small set
793 of extensions, it also adjust the internal state of file, but can be
794 called outside an `changing_parents` context.
795
796 A very small number of extension meddle with the working copy content
797 in a way that requires to adjust the dirstate accordingly. At the time
798 this command is written they are :
799 - keyword,
800 - largefile,
801 PLEASE DO NOT GROW THIS LIST ANY FURTHER.
802
803 This function could probably be replaced by more semantic one (like
804 "adjust expected size" or "always revalidate file content", etc)
805 however at the time where this is writen, this is too much of a detour
806 to be considered.
807 """
808 if not (self._changing_level > 0 or self._running_status > 0):
809 msg = "requires a changes context"
810 raise error.ProgrammingError(msg)
811 self._update_file(
812 *args,
813 **kwargs,
814 )
815
816 def _update_file(
817 self,
818 filename,
819 wc_tracked,
820 p1_tracked,
821 p2_info=False,
822 possibly_dirty=False,
823 parentfiledata=None,
824 ):
555 825
556 826 # note: I do not think we need to double check name clash here since we
557 827 # are in a update/merge case that should already have taken care of
@@ -680,12 +950,16 b' class dirstate:'
680 950 return self._normalize(path, isknown, ignoremissing)
681 951 return path
682 952
953 # XXX this method is barely used, as a result:
954 # - its semantic is unclear
955 # - do we really needs it ?
956 @requires_changing_parents
683 957 def clear(self):
684 958 self._map.clear()
685 959 self._dirty = True
686 960
961 @requires_changing_parents
687 962 def rebuild(self, parent, allfiles, changedfiles=None):
688
689 963 matcher = self._sparsematcher
690 964 if matcher is not None and not matcher.always():
691 965 # should not add non-matching files
@@ -724,7 +998,6 b' class dirstate:'
724 998 self._map.setparents(parent, self._nodeconstants.nullid)
725 999
726 1000 for f in to_lookup:
727
728 1001 if self.in_merge:
729 1002 self.set_tracked(f)
730 1003 else:
@@ -749,20 +1022,41 b' class dirstate:'
749 1022 def write(self, tr):
750 1023 if not self._dirty:
751 1024 return
1025 # make sure we don't request a write of invalidated content
1026 # XXX move before the dirty check once `unlock` stop calling `write`
1027 assert not self._invalidated_context
752 1028
753 1029 write_key = self._use_tracked_hint and self._dirty_tracked_set
754 1030 if tr:
1031
1032 def on_abort(tr):
1033 self._attached_to_a_transaction = False
1034 self.invalidate()
1035
1036 # make sure we invalidate the current change on abort
1037 if tr is not None:
1038 tr.addabort(
1039 b'dirstate-invalidate%s' % self._tr_key_suffix,
1040 on_abort,
1041 )
1042
1043 self._attached_to_a_transaction = True
1044
1045 def on_success(f):
1046 self._attached_to_a_transaction = False
1047 self._writedirstate(tr, f),
1048
755 1049 # delay writing in-memory changes out
756 1050 tr.addfilegenerator(
757 b'dirstate-1-main',
1051 b'dirstate-1-main%s' % self._tr_key_suffix,
758 1052 (self._filename,),
759 lambda f: self._writedirstate(tr, f),
1053 on_success,
760 1054 location=b'plain',
761 1055 post_finalize=True,
762 1056 )
763 1057 if write_key:
764 1058 tr.addfilegenerator(
765 b'dirstate-2-key-post',
1059 b'dirstate-2-key-post%s' % self._tr_key_suffix,
766 1060 (self._filename_th,),
767 1061 lambda f: self._write_tracked_hint(tr, f),
768 1062 location=b'plain',
@@ -798,6 +1092,8 b' class dirstate:'
798 1092 self._plchangecallbacks[category] = callback
799 1093
800 1094 def _writedirstate(self, tr, st):
1095 # make sure we don't write invalidated content
1096 assert not self._invalidated_context
801 1097 # notify callbacks about parents change
802 1098 if self._origpl is not None and self._origpl != self._pl:
803 1099 for c, callback in sorted(self._plchangecallbacks.items()):
@@ -936,7 +1232,8 b' class dirstate:'
936 1232 badfn(ff, badtype(kind))
937 1233 if nf in dmap:
938 1234 results[nf] = None
939 except OSError as inst: # nf not found on disk - it is dirstate only
1235 except (OSError) as inst:
1236 # nf not found on disk - it is dirstate only
940 1237 if nf in dmap: # does it exactly match a missing file?
941 1238 results[nf] = None
942 1239 else: # does it match a missing directory?
@@ -1246,7 +1543,7 b' class dirstate:'
1246 1543 )
1247 1544 )
1248 1545
1249 for (fn, message) in bad:
1546 for fn, message in bad:
1250 1547 matcher.bad(fn, encoding.strtolocal(message))
1251 1548
1252 1549 status = scmutil.status(
@@ -1276,6 +1573,9 b' class dirstate:'
1276 1573 files that have definitely not been modified since the
1277 1574 dirstate was written
1278 1575 """
1576 if not self._running_status:
1577 msg = "Calling `status` outside a `running_status` context"
1578 raise error.ProgrammingError(msg)
1279 1579 listignored, listclean, listunknown = ignored, clean, unknown
1280 1580 lookup, modified, added, unknown, ignored = [], [], [], [], []
1281 1581 removed, deleted, clean = [], [], []
@@ -1435,142 +1735,47 b' class dirstate:'
1435 1735 else:
1436 1736 return self._filename
1437 1737
1438 def data_backup_filename(self, backupname):
1439 if not self._use_dirstate_v2:
1440 return None
1441 return backupname + b'.v2-data'
1442
1443 def _new_backup_data_filename(self, backupname):
1444 """return a filename to backup a data-file or None"""
1445 if not self._use_dirstate_v2:
1446 return None
1447 if self._map.docket.uuid is None:
1448 # not created yet, nothing to backup
1449 return None
1450 data_filename = self._map.docket.data_filename()
1451 return data_filename, self.data_backup_filename(backupname)
1452
1453 def backup_data_file(self, backupname):
1454 if not self._use_dirstate_v2:
1455 return None
1456 docket = docketmod.DirstateDocket.parse(
1457 self._opener.read(backupname),
1458 self._nodeconstants,
1459 )
1460 return self.data_backup_filename(backupname), docket.data_filename()
1461
1462 def savebackup(self, tr, backupname):
1463 '''Save current dirstate into backup file'''
1464 filename = self._actualfilename(tr)
1465 assert backupname != filename
1738 def all_file_names(self):
1739 """list all filename currently used by this dirstate
1466 1740
1467 # use '_writedirstate' instead of 'write' to write changes certainly,
1468 # because the latter omits writing out if transaction is running.
1469 # output file will be used to create backup of dirstate at this point.
1470 if self._dirty or not self._opener.exists(filename):
1471 self._writedirstate(
1472 tr,
1473 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1741 This is only used to do `hg rollback` related backup in the transaction
1742 """
1743 if not self._opener.exists(self._filename):
1744 # no data every written to disk yet
1745 return ()
1746 elif self._use_dirstate_v2:
1747 return (
1748 self._filename,
1749 self._map.docket.data_filename(),
1474 1750 )
1751 else:
1752 return (self._filename,)
1475 1753
1476 if tr:
1477 # ensure that subsequent tr.writepending returns True for
1478 # changes written out above, even if dirstate is never
1479 # changed after this
1480 tr.addfilegenerator(
1481 b'dirstate-1-main',
1482 (self._filename,),
1483 lambda f: self._writedirstate(tr, f),
1484 location=b'plain',
1485 post_finalize=True,
1486 )
1487
1488 # ensure that pending file written above is unlinked at
1489 # failure, even if tr.writepending isn't invoked until the
1490 # end of this transaction
1491 tr.registertmp(filename, location=b'plain')
1492
1493 self._opener.tryunlink(backupname)
1494 # hardlink backup is okay because _writedirstate is always called
1495 # with an "atomictemp=True" file.
1496 util.copyfile(
1497 self._opener.join(filename),
1498 self._opener.join(backupname),
1499 hardlink=True,
1754 def verify(self, m1, m2, p1, narrow_matcher=None):
1755 """
1756 check the dirstate contents against the parent manifest and yield errors
1757 """
1758 missing_from_p1 = _(
1759 b"%s marked as tracked in p1 (%s) but not in manifest1\n"
1500 1760 )
1501 data_pair = self._new_backup_data_filename(backupname)
1502 if data_pair is not None:
1503 data_filename, bck_data_filename = data_pair
1504 util.copyfile(
1505 self._opener.join(data_filename),
1506 self._opener.join(bck_data_filename),
1507 hardlink=True,
1508 )
1509 if tr is not None:
1510 # ensure that pending file written above is unlinked at
1511 # failure, even if tr.writepending isn't invoked until the
1512 # end of this transaction
1513 tr.registertmp(bck_data_filename, location=b'plain')
1514
1515 def restorebackup(self, tr, backupname):
1516 '''Restore dirstate by backup file'''
1517 # this "invalidate()" prevents "wlock.release()" from writing
1518 # changes of dirstate out after restoring from backup file
1519 self.invalidate()
1520 o = self._opener
1521 if not o.exists(backupname):
1522 # there was no file backup, delete existing files
1523 filename = self._actualfilename(tr)
1524 data_file = None
1525 if self._use_dirstate_v2 and self._map.docket.uuid is not None:
1526 data_file = self._map.docket.data_filename()
1527 if o.exists(filename):
1528 o.unlink(filename)
1529 if data_file is not None and o.exists(data_file):
1530 o.unlink(data_file)
1531 return
1532 filename = self._actualfilename(tr)
1533 data_pair = self.backup_data_file(backupname)
1534 if o.exists(filename) and util.samefile(
1535 o.join(backupname), o.join(filename)
1536 ):
1537 o.unlink(backupname)
1538 else:
1539 o.rename(backupname, filename, checkambig=True)
1540
1541 if data_pair is not None:
1542 data_backup, target = data_pair
1543 if o.exists(target) and util.samefile(
1544 o.join(data_backup), o.join(target)
1545 ):
1546 o.unlink(data_backup)
1547 else:
1548 o.rename(data_backup, target, checkambig=True)
1549
1550 def clearbackup(self, tr, backupname):
1551 '''Clear backup file'''
1552 o = self._opener
1553 if o.exists(backupname):
1554 data_backup = self.backup_data_file(backupname)
1555 o.unlink(backupname)
1556 if data_backup is not None:
1557 o.unlink(data_backup[0])
1558
1559 def verify(self, m1, m2):
1560 """check the dirstate content again the parent manifest and yield errors"""
1561 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1562 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1563 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1564 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1761 unexpected_in_p1 = _(b"%s marked as added, but also in manifest1\n")
1762 missing_from_ps = _(
1763 b"%s marked as modified, but not in either manifest\n"
1764 )
1765 missing_from_ds = _(
1766 b"%s in manifest1, but not marked as tracked in p1 (%s)\n"
1767 )
1565 1768 for f, entry in self.items():
1566 state = entry.state
1567 if state in b"nr" and f not in m1:
1568 yield (missing_from_p1, f, state)
1569 if state in b"a" and f in m1:
1570 yield (unexpected_in_p1, f, state)
1571 if state in b"m" and f not in m1 and f not in m2:
1572 yield (missing_from_ps, f, state)
1769 if entry.p1_tracked:
1770 if entry.modified and f not in m1 and f not in m2:
1771 yield missing_from_ps % f
1772 elif f not in m1:
1773 yield missing_from_p1 % (f, node.short(p1))
1774 if entry.added and f in m1:
1775 yield unexpected_in_p1 % f
1573 1776 for f in m1:
1574 state = self.get_entry(f).state
1575 if state not in b"nrm":
1576 yield (missing_from_ds, f, state)
1777 if narrow_matcher is not None and not narrow_matcher(f):
1778 continue
1779 entry = self.get_entry(f)
1780 if not entry.p1_tracked:
1781 yield missing_from_ds % (f, node.short(p1))
@@ -58,6 +58,34 b' class _dirstatemapcommon:'
58 58 # for consistent view between _pl() and _read() invocations
59 59 self._pendingmode = None
60 60
61 def _set_identity(self):
62 self.identity = self._get_current_identity()
63
64 def _get_current_identity(self):
65 try:
66 return util.cachestat(self._opener.join(self._filename))
67 except FileNotFoundError:
68 return None
69
70 def may_need_refresh(self):
71 if 'identity' not in vars(self):
72 # no existing identity, we need a refresh
73 return True
74 if self.identity is None:
75 return True
76 if not self.identity.cacheable():
77 # We cannot trust the entry
78 # XXX this is a problem on windows, NFS, or other inode less system
79 return True
80 current_identity = self._get_current_identity()
81 if current_identity is None:
82 return True
83 if not current_identity.cacheable():
84 # We cannot trust the entry
85 # XXX this is a problem on windows, NFS, or other inode less system
86 return True
87 return current_identity != self.identity
88
61 89 def preload(self):
62 90 """Loads the underlying data, if it's not already loaded"""
63 91 self._map
@@ -118,6 +146,9 b' class _dirstatemapcommon:'
118 146 raise error.ProgrammingError(b'dirstate docket name collision')
119 147 data_filename = new_docket.data_filename()
120 148 self._opener.write(data_filename, packed)
149 # tell the transaction that we are adding a new file
150 if tr is not None:
151 tr.addbackup(data_filename, location=b'plain')
121 152 # Write the new docket after the new data file has been
122 153 # written. Because `st` was opened with `atomictemp=True`,
123 154 # the actual `.hg/dirstate` file is only affected on close.
@@ -127,6 +158,8 b' class _dirstatemapcommon:'
127 158 # the new data file was written.
128 159 if old_docket.uuid:
129 160 data_filename = old_docket.data_filename()
161 if tr is not None:
162 tr.addbackup(data_filename, location=b'plain')
130 163 unlink = lambda _tr=None: self._opener.unlink(data_filename)
131 164 if tr:
132 165 category = b"dirstate-v2-clean-" + old_docket.uuid
@@ -258,9 +291,7 b' class dirstatemap(_dirstatemapcommon):'
258 291
259 292 def read(self):
260 293 # ignore HG_PENDING because identity is used only for writing
261 self.identity = util.filestat.frompath(
262 self._opener.join(self._filename)
263 )
294 self._set_identity()
264 295
265 296 if self._use_dirstate_v2:
266 297 if not self.docket.uuid:
@@ -523,9 +554,7 b' if rustmod is not None:'
523 554 Fills the Dirstatemap when called.
524 555 """
525 556 # ignore HG_PENDING because identity is used only for writing
526 self.identity = util.filestat.frompath(
527 self._opener.join(self._filename)
528 )
557 self._set_identity()
529 558
530 559 if self._use_dirstate_v2:
531 560 if self.docket.uuid:
@@ -614,6 +643,14 b' if rustmod is not None:'
614 643 if append:
615 644 docket = self.docket
616 645 data_filename = docket.data_filename()
646 # We mark it for backup to make sure a future `hg rollback` (or
647 # `hg recover`?) call find the data it needs to restore a
648 # working repository.
649 #
650 # The backup can use a hardlink because the format is resistant
651 # to trailing "dead" data.
652 if tr is not None:
653 tr.addbackup(data_filename, location=b'plain')
617 654 with self._opener(data_filename, b'r+b') as fp:
618 655 fp.seek(docket.data_size)
619 656 assert fp.tell() == docket.data_size
@@ -980,7 +980,8 b' def _getlocal(ui, rpath, wd=None):'
980 980 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
981 981
982 982 if rpath:
983 path = urlutil.get_clone_path(lui, rpath)[0]
983 path_obj = urlutil.get_clone_path_obj(lui, rpath)
984 path = path_obj.rawloc
984 985 lui = ui.copy()
985 986 if rcutil.use_repo_hgrc():
986 987 _readsharedsourceconfig(lui, path)
@@ -1183,7 +1183,12 b' def _pushbundle2(pushop):'
1183 1183 trgetter = None
1184 1184 if pushback:
1185 1185 trgetter = pushop.trmanager.transaction
1186 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1186 op = bundle2.processbundle(
1187 pushop.repo,
1188 reply,
1189 trgetter,
1190 remote=pushop.remote,
1191 )
1187 1192 except error.BundleValueError as exc:
1188 1193 raise error.RemoteError(_(b'missing support for %s') % exc)
1189 1194 except bundle2.AbortFromPart as exc:
@@ -1903,10 +1908,18 b' def _pullbundle2(pullop):'
1903 1908
1904 1909 try:
1905 1910 op = bundle2.bundleoperation(
1906 pullop.repo, pullop.gettransaction, source=b'pull'
1911 pullop.repo,
1912 pullop.gettransaction,
1913 source=b'pull',
1914 remote=pullop.remote,
1907 1915 )
1908 1916 op.modes[b'bookmarks'] = b'records'
1909 bundle2.processbundle(pullop.repo, bundle, op=op)
1917 bundle2.processbundle(
1918 pullop.repo,
1919 bundle,
1920 op=op,
1921 remote=pullop.remote,
1922 )
1910 1923 except bundle2.AbortFromPart as exc:
1911 1924 pullop.repo.ui.error(_(b'remote: abort: %s\n') % exc)
1912 1925 raise error.RemoteError(_(b'pull failed on remote'), hint=exc.hint)
@@ -1995,7 +2008,12 b' def _pullchangeset(pullop):'
1995 2008 ).result()
1996 2009
1997 2010 bundleop = bundle2.applybundle(
1998 pullop.repo, cg, tr, b'pull', pullop.remote.url()
2011 pullop.repo,
2012 cg,
2013 tr,
2014 b'pull',
2015 pullop.remote.url(),
2016 remote=pullop.remote,
1999 2017 )
2000 2018 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2001 2019
@@ -111,6 +111,7 b' class filelog:'
111 111 assumehaveparentrevisions=False,
112 112 deltamode=repository.CG_DELTAMODE_STD,
113 113 sidedata_helpers=None,
114 debug_info=None,
114 115 ):
115 116 return self._revlog.emitrevisions(
116 117 nodes,
@@ -119,6 +120,7 b' class filelog:'
119 120 assumehaveparentrevisions=assumehaveparentrevisions,
120 121 deltamode=deltamode,
121 122 sidedata_helpers=sidedata_helpers,
123 debug_info=debug_info,
122 124 )
123 125
124 126 def addrevision(
@@ -151,6 +153,8 b' class filelog:'
151 153 addrevisioncb=None,
152 154 duplicaterevisioncb=None,
153 155 maybemissingparents=False,
156 debug_info=None,
157 delta_base_reuse_policy=None,
154 158 ):
155 159 if maybemissingparents:
156 160 raise error.Abort(
@@ -171,6 +175,8 b' class filelog:'
171 175 transaction,
172 176 addrevisioncb=addrevisioncb,
173 177 duplicaterevisioncb=duplicaterevisioncb,
178 debug_info=debug_info,
179 delta_base_reuse_policy=delta_base_reuse_policy,
174 180 )
175 181
176 182 def getstrippoint(self, minlink):
@@ -158,7 +158,7 b' def findexternaltool(ui, tool):'
158 158 continue
159 159 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
160 160 if p:
161 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend", b""))
161 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend"))
162 162 if p:
163 163 return p
164 164 exe = _toolstr(ui, tool, b"executable", tool)
@@ -478,8 +478,9 b' def _merge(repo, local, other, base, mod'
478 478 """
479 479 Uses the internal non-interactive simple merge algorithm for merging
480 480 files. It will fail if there are any conflicts and leave markers in
481 the partially merged file. Markers will have two sections, one for each side
482 of merge, unless mode equals 'union' which suppresses the markers."""
481 the partially merged file. Markers will have two sections, one for each
482 side of merge, unless mode equals 'union' or 'union-other-first' which
483 suppresses the markers."""
483 484 ui = repo.ui
484 485
485 486 try:
@@ -510,12 +511,28 b' def _merge(repo, local, other, base, mod'
510 511 def _iunion(repo, mynode, local, other, base, toolconf, backup):
511 512 """
512 513 Uses the internal non-interactive simple merge algorithm for merging
513 files. It will use both left and right sides for conflict regions.
514 files. It will use both local and other sides for conflict regions by
515 adding local on top of other.
514 516 No markers are inserted."""
515 517 return _merge(repo, local, other, base, b'union')
516 518
517 519
518 520 @internaltool(
521 b'union-other-first',
522 fullmerge,
523 _(
524 b"warning: conflicts while merging %s! "
525 b"(edit, then use 'hg resolve --mark')\n"
526 ),
527 precheck=_mergecheck,
528 )
529 def _iunion_other_first(repo, mynode, local, other, base, toolconf, backup):
530 """
531 Like :union, but add other on top of local."""
532 return _merge(repo, local, other, base, b'union-other-first')
533
534
535 @internaltool(
519 536 b'merge',
520 537 fullmerge,
521 538 _(
@@ -10,6 +10,18 b' import itertools'
10 10 import re
11 11 import textwrap
12 12
13 from typing import (
14 Callable,
15 Dict,
16 Iterable,
17 List,
18 Optional,
19 Set,
20 Tuple,
21 Union,
22 cast,
23 )
24
13 25 from .i18n import (
14 26 _,
15 27 gettext,
@@ -40,7 +52,16 b' from .utils import ('
40 52 stringutil,
41 53 )
42 54
43 _exclkeywords = {
55 _DocLoader = Callable[[uimod.ui], bytes]
56 # Old extensions may not register with a category
57 _HelpEntry = Union["_HelpEntryNoCategory", "_HelpEntryWithCategory"]
58 _HelpEntryNoCategory = Tuple[List[bytes], bytes, _DocLoader]
59 _HelpEntryWithCategory = Tuple[List[bytes], bytes, _DocLoader, bytes]
60 _SelectFn = Callable[[object], bool]
61 _SynonymTable = Dict[bytes, List[bytes]]
62 _TopicHook = Callable[[uimod.ui, bytes, bytes], bytes]
63
64 _exclkeywords: Set[bytes] = {
44 65 b"(ADVANCED)",
45 66 b"(DEPRECATED)",
46 67 b"(EXPERIMENTAL)",
@@ -56,7 +77,7 b' from .utils import ('
56 77 # Extensions with custom categories should insert them into this list
57 78 # after/before the appropriate item, rather than replacing the list or
58 79 # assuming absolute positions.
59 CATEGORY_ORDER = [
80 CATEGORY_ORDER: List[bytes] = [
60 81 registrar.command.CATEGORY_REPO_CREATION,
61 82 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
62 83 registrar.command.CATEGORY_COMMITTING,
@@ -74,7 +95,7 b' CATEGORY_ORDER = ['
74 95
75 96 # Human-readable category names. These are translated.
76 97 # Extensions with custom categories should add their names here.
77 CATEGORY_NAMES = {
98 CATEGORY_NAMES: Dict[bytes, bytes] = {
78 99 registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
79 100 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
80 101 registrar.command.CATEGORY_COMMITTING: b'Change creation',
@@ -102,7 +123,7 b" TOPIC_CATEGORY_NONE = b'none'"
102 123 # Extensions with custom categories should insert them into this list
103 124 # after/before the appropriate item, rather than replacing the list or
104 125 # assuming absolute positions.
105 TOPIC_CATEGORY_ORDER = [
126 TOPIC_CATEGORY_ORDER: List[bytes] = [
106 127 TOPIC_CATEGORY_IDS,
107 128 TOPIC_CATEGORY_OUTPUT,
108 129 TOPIC_CATEGORY_CONFIG,
@@ -112,7 +133,7 b' TOPIC_CATEGORY_ORDER = ['
112 133 ]
113 134
114 135 # Human-readable topic category names. These are translated.
115 TOPIC_CATEGORY_NAMES = {
136 TOPIC_CATEGORY_NAMES: Dict[bytes, bytes] = {
116 137 TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
117 138 TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
118 139 TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
@@ -122,7 +143,12 b' TOPIC_CATEGORY_NAMES = {'
122 143 }
123 144
124 145
125 def listexts(header, exts, indent=1, showdeprecated=False):
146 def listexts(
147 header: bytes,
148 exts: Dict[bytes, bytes],
149 indent: int = 1,
150 showdeprecated: bool = False,
151 ) -> List[bytes]:
126 152 '''return a text listing of the given extensions'''
127 153 rst = []
128 154 if exts:
@@ -135,7 +161,7 b' def listexts(header, exts, indent=1, sho'
135 161 return rst
136 162
137 163
138 def extshelp(ui):
164 def extshelp(ui: uimod.ui) -> bytes:
139 165 rst = loaddoc(b'extensions')(ui).splitlines(True)
140 166 rst.extend(
141 167 listexts(
@@ -153,7 +179,7 b' def extshelp(ui):'
153 179 return doc
154 180
155 181
156 def parsedefaultmarker(text):
182 def parsedefaultmarker(text: bytes) -> Optional[Tuple[bytes, List[bytes]]]:
157 183 """given a text 'abc (DEFAULT: def.ghi)',
158 184 returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
159 185 if text[-1:] == b')':
@@ -164,7 +190,7 b' def parsedefaultmarker(text):'
164 190 return text[:pos], item.split(b'.', 2)
165 191
166 192
167 def optrst(header, options, verbose, ui):
193 def optrst(header: bytes, options, verbose: bool, ui: uimod.ui) -> bytes:
168 194 data = []
169 195 multioccur = False
170 196 for option in options:
@@ -220,13 +246,15 b' def optrst(header, options, verbose, ui)'
220 246 return b''.join(rst)
221 247
222 248
223 def indicateomitted(rst, omitted, notomitted=None):
249 def indicateomitted(
250 rst: List[bytes], omitted: bytes, notomitted: Optional[bytes] = None
251 ) -> None:
224 252 rst.append(b'\n\n.. container:: omitted\n\n %s\n\n' % omitted)
225 253 if notomitted:
226 254 rst.append(b'\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
227 255
228 256
229 def filtercmd(ui, cmd, func, kw, doc):
257 def filtercmd(ui: uimod.ui, cmd: bytes, func, kw: bytes, doc: bytes) -> bool:
230 258 if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
231 259 # Debug command, and user is not looking for those.
232 260 return True
@@ -249,11 +277,13 b' def filtercmd(ui, cmd, func, kw, doc):'
249 277 return False
250 278
251 279
252 def filtertopic(ui, topic):
280 def filtertopic(ui: uimod.ui, topic: bytes) -> bool:
253 281 return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
254 282
255 283
256 def topicmatch(ui, commands, kw):
284 def topicmatch(
285 ui: uimod.ui, commands, kw: bytes
286 ) -> Dict[bytes, List[Tuple[bytes, bytes]]]:
257 287 """Return help topics matching kw.
258 288
259 289 Returns {'section': [(name, summary), ...], ...} where section is
@@ -326,10 +356,10 b' def topicmatch(ui, commands, kw):'
326 356 return results
327 357
328 358
329 def loaddoc(topic, subdir=None):
359 def loaddoc(topic: bytes, subdir: Optional[bytes] = None) -> _DocLoader:
330 360 """Return a delayed loader for help/topic.txt."""
331 361
332 def loader(ui):
362 def loader(ui: uimod.ui) -> bytes:
333 363 package = b'mercurial.helptext'
334 364 if subdir:
335 365 package += b'.' + subdir
@@ -342,7 +372,7 b' def loaddoc(topic, subdir=None):'
342 372 return loader
343 373
344 374
345 internalstable = sorted(
375 internalstable: List[_HelpEntryNoCategory] = sorted(
346 376 [
347 377 (
348 378 [b'bid-merge'],
@@ -407,7 +437,7 b' internalstable = sorted('
407 437 )
408 438
409 439
410 def internalshelp(ui):
440 def internalshelp(ui: uimod.ui) -> bytes:
411 441 """Generate the index for the "internals" topic."""
412 442 lines = [
413 443 b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
@@ -419,7 +449,7 b' def internalshelp(ui):'
419 449 return b''.join(lines)
420 450
421 451
422 helptable = sorted(
452 helptable: List[_HelpEntryWithCategory] = sorted(
423 453 [
424 454 (
425 455 [b'bundlespec'],
@@ -581,20 +611,27 b' helptable = sorted('
581 611 )
582 612
583 613 # Maps topics with sub-topics to a list of their sub-topics.
584 subtopics = {
614 subtopics: Dict[bytes, List[_HelpEntryNoCategory]] = {
585 615 b'internals': internalstable,
586 616 }
587 617
588 618 # Map topics to lists of callable taking the current topic help and
589 619 # returning the updated version
590 helphooks = {}
620 helphooks: Dict[bytes, List[_TopicHook]] = {}
591 621
592 622
593 def addtopichook(topic, rewriter):
623 def addtopichook(topic: bytes, rewriter: _TopicHook) -> None:
594 624 helphooks.setdefault(topic, []).append(rewriter)
595 625
596 626
597 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
627 def makeitemsdoc(
628 ui: uimod.ui,
629 topic: bytes,
630 doc: bytes,
631 marker: bytes,
632 items: Dict[bytes, bytes],
633 dedent: bool = False,
634 ) -> bytes:
598 635 """Extract docstring from the items key to function mapping, build a
599 636 single documentation block and use it to overwrite the marker in doc.
600 637 """
@@ -622,8 +659,10 b' def makeitemsdoc(ui, topic, doc, marker,'
622 659 return doc.replace(marker, entries)
623 660
624 661
625 def addtopicsymbols(topic, marker, symbols, dedent=False):
626 def add(ui, topic, doc):
662 def addtopicsymbols(
663 topic: bytes, marker: bytes, symbols, dedent: bool = False
664 ) -> None:
665 def add(ui: uimod.ui, topic: bytes, doc: bytes):
627 666 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
628 667
629 668 addtopichook(topic, add)
@@ -647,7 +686,7 b' addtopicsymbols('
647 686 )
648 687
649 688
650 def inserttweakrc(ui, topic, doc):
689 def inserttweakrc(ui: uimod.ui, topic: bytes, doc: bytes) -> bytes:
651 690 marker = b'.. tweakdefaultsmarker'
652 691 repl = uimod.tweakrc
653 692
@@ -658,7 +697,9 b' def inserttweakrc(ui, topic, doc):'
658 697 return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
659 698
660 699
661 def _getcategorizedhelpcmds(ui, cmdtable, name, select=None):
700 def _getcategorizedhelpcmds(
701 ui: uimod.ui, cmdtable, name: bytes, select: Optional[_SelectFn] = None
702 ) -> Tuple[Dict[bytes, List[bytes]], Dict[bytes, bytes], _SynonymTable]:
662 703 # Category -> list of commands
663 704 cats = {}
664 705 # Command -> short description
@@ -687,16 +728,18 b' def _getcategorizedhelpcmds(ui, cmdtable'
687 728 return cats, h, syns
688 729
689 730
690 def _getcategorizedhelptopics(ui, topictable):
731 def _getcategorizedhelptopics(
732 ui: uimod.ui, topictable: List[_HelpEntry]
733 ) -> Tuple[Dict[bytes, List[Tuple[bytes, bytes]]], Dict[bytes, List[bytes]]]:
691 734 # Group commands by category.
692 735 topiccats = {}
693 736 syns = {}
694 737 for topic in topictable:
695 738 names, header, doc = topic[0:3]
696 739 if len(topic) > 3 and topic[3]:
697 category = topic[3]
740 category: bytes = cast(bytes, topic[3]) # help pytype
698 741 else:
699 category = TOPIC_CATEGORY_NONE
742 category: bytes = TOPIC_CATEGORY_NONE
700 743
701 744 topicname = names[0]
702 745 syns[topicname] = list(names)
@@ -709,15 +752,15 b" addtopichook(b'config', inserttweakrc)"
709 752
710 753
711 754 def help_(
712 ui,
755 ui: uimod.ui,
713 756 commands,
714 name,
715 unknowncmd=False,
716 full=True,
717 subtopic=None,
718 fullname=None,
757 name: bytes,
758 unknowncmd: bool = False,
759 full: bool = True,
760 subtopic: Optional[bytes] = None,
761 fullname: Optional[bytes] = None,
719 762 **opts
720 ):
763 ) -> bytes:
721 764 """
722 765 Generate the help for 'name' as unformatted restructured text. If
723 766 'name' is None, describe the commands available.
@@ -725,7 +768,7 b' def help_('
725 768
726 769 opts = pycompat.byteskwargs(opts)
727 770
728 def helpcmd(name, subtopic=None):
771 def helpcmd(name: bytes, subtopic: Optional[bytes]) -> List[bytes]:
729 772 try:
730 773 aliases, entry = cmdutil.findcmd(
731 774 name, commands.table, strict=unknowncmd
@@ -826,7 +869,7 b' def help_('
826 869
827 870 return rst
828 871
829 def helplist(select=None, **opts):
872 def helplist(select: Optional[_SelectFn] = None, **opts) -> List[bytes]:
830 873 cats, h, syns = _getcategorizedhelpcmds(
831 874 ui, commands.table, name, select
832 875 )
@@ -846,7 +889,7 b' def help_('
846 889 else:
847 890 rst.append(_(b'list of commands:\n'))
848 891
849 def appendcmds(cmds):
892 def appendcmds(cmds: Iterable[bytes]) -> None:
850 893 cmds = sorted(cmds)
851 894 for c in cmds:
852 895 display_cmd = c
@@ -955,7 +998,7 b' def help_('
955 998 )
956 999 return rst
957 1000
958 def helptopic(name, subtopic=None):
1001 def helptopic(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
959 1002 # Look for sub-topic entry first.
960 1003 header, doc = None, None
961 1004 if subtopic and name in subtopics:
@@ -998,7 +1041,7 b' def help_('
998 1041 pass
999 1042 return rst
1000 1043
1001 def helpext(name, subtopic=None):
1044 def helpext(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1002 1045 try:
1003 1046 mod = extensions.find(name)
1004 1047 doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
@@ -1040,7 +1083,9 b' def help_('
1040 1083 )
1041 1084 return rst
1042 1085
1043 def helpextcmd(name, subtopic=None):
1086 def helpextcmd(
1087 name: bytes, subtopic: Optional[bytes] = None
1088 ) -> List[bytes]:
1044 1089 cmd, ext, doc = extensions.disabledcmd(
1045 1090 ui, name, ui.configbool(b'ui', b'strict')
1046 1091 )
@@ -1127,8 +1172,14 b' def help_('
1127 1172
1128 1173
1129 1174 def formattedhelp(
1130 ui, commands, fullname, keep=None, unknowncmd=False, full=True, **opts
1131 ):
1175 ui: uimod.ui,
1176 commands,
1177 fullname: Optional[bytes],
1178 keep: Optional[Iterable[bytes]] = None,
1179 unknowncmd: bool = False,
1180 full: bool = True,
1181 **opts
1182 ) -> bytes:
1132 1183 """get help for a given topic (as a dotted name) as rendered rst
1133 1184
1134 1185 Either returns the rendered help text or raises an exception.
@@ -1922,6 +1922,42 b' The following sub-options can be defined'
1922 1922 - ``ignore``: ignore bookmarks during exchange.
1923 1923 (This currently only affect pulling)
1924 1924
1925 .. container:: verbose
1926
1927 ``delta-reuse-policy``
1928 Control the policy regarding deltas sent by the remote during pulls.
1929
1930 This is an advanced option that non-admin users should not need to understand
1931 or set. This option can be used to speed up pulls from trusted central
1932 servers, or to fix-up deltas from older servers.
1933
1934 It supports the following values:
1935
1936 - ``default``: use the policy defined by
1937 `storage.revlog.reuse-external-delta-parent`,
1938
1939 - ``no-reuse``: start a new optimal delta search for each new revision we add
1940 to the repository. The deltas from the server will be reused when the base
1941 it applies to is tested (this can be frequent if that base is the one and
1942 unique parent of that revision). This can significantly slowdown pulls but
1943 will result in an optimized storage space if the remote peer is sending poor
1944 quality deltas.
1945
1946 - ``try-base``: try to reuse the deltas from the remote peer as long as they
1947 create a valid delta-chain in the local repository. This speeds up the
1948 unbundling process, but can result in sub-optimal storage space if the
1949 remote peer is sending poor quality deltas.
1950
1951 - ``forced``: the deltas from the peer will be reused in all cases, even if
1952 the resulting delta-chain is "invalid". This setting will ensure the bundle
1953 is applied at minimal CPU cost, but it can result in longer delta chains
1954 being created on the client, making revisions potentially slower to access
1955 in the future. If you think you need this option, you should make sure you
1956 are also talking to the Mercurial developer community to get confirmation.
1957
1958 See `hg help config.storage.revlog.reuse-external-delta-parent` for a similar
1959 global option. That option defines the behavior of `default`.
1960
1925 1961 The following special named paths exist:
1926 1962
1927 1963 ``default``
@@ -2281,6 +2317,21 b' category impact performance and reposito'
2281 2317 To fix affected revisions that already exist within the repository, one can
2282 2318 use :hg:`debug-repair-issue-6528`.
2283 2319
2320 .. container:: verbose
2321
2322 ``revlog.delta-parent-search.candidate-group-chunk-size``
2323 Tune the number of delta bases the storage will consider in the
2324 same "round" of search. In some very rare cases, using a smaller value
2325 might result in faster processing at the possible expense of storage
2326 space, while using larger values might result in slower processing at the
2327 possible benefit of storage space. A value of "0" means no limitation.
2328
2329 default: no limitation
2330
2331 This is unlikely that you'll have to tune this configuration. If you think
2332 you do, consider talking with the mercurial developer community about your
2333 repositories.
2334
2284 2335 ``revlog.optimize-delta-parent-choice``
2285 2336 When storing a merge revision, both parents will be equally considered as
2286 2337 a possible delta base. This results in better delta selection and improved
@@ -76,8 +76,8 b' instructions on how to install from sour'
76 76 MSRV
77 77 ====
78 78
79 The minimum supported Rust version is currently 1.48.0. The project's policy is
80 to follow the version from Debian stable, to make the distributions' job easier.
79 The minimum supported Rust version is currently 1.61.0. The project's policy is
80 to follow the version from Debian testing, to make the distributions' job easier.
81 81
82 82 rhg
83 83 ===
@@ -65,28 +65,12 b' release = lock.release'
65 65 sharedbookmarks = b'bookmarks'
66 66
67 67
68 def _local(path):
69 path = util.expandpath(urlutil.urllocalpath(path))
70
71 try:
72 # we use os.stat() directly here instead of os.path.isfile()
73 # because the latter started returning `False` on invalid path
74 # exceptions starting in 3.8 and we care about handling
75 # invalid paths specially here.
76 st = os.stat(path)
77 isfile = stat.S_ISREG(st.st_mode)
78 except ValueError as e:
79 raise error.Abort(
80 _(b'invalid path %s: %s') % (path, stringutil.forcebytestr(e))
81 )
82 except OSError:
83 isfile = False
84
85 return isfile and bundlerepo or localrepo
86
87
88 68 def addbranchrevs(lrepo, other, branches, revs):
89 peer = other.peer() # a courtesy to callers using a localrepo for other
69 if util.safehasattr(other, 'peer'):
70 # a courtesy to callers using a localrepo for other
71 peer = other.peer()
72 else:
73 peer = other
90 74 hashbranch, branches = branches
91 75 if not hashbranch and not branches:
92 76 x = revs or None
@@ -129,10 +113,47 b' def addbranchrevs(lrepo, other, branches'
129 113 return revs, revs[0]
130 114
131 115
132 schemes = {
116 def _isfile(path):
117 try:
118 # we use os.stat() directly here instead of os.path.isfile()
119 # because the latter started returning `False` on invalid path
120 # exceptions starting in 3.8 and we care about handling
121 # invalid paths specially here.
122 st = os.stat(path)
123 except ValueError as e:
124 msg = stringutil.forcebytestr(e)
125 raise error.Abort(_(b'invalid path %s: %s') % (path, msg))
126 except OSError:
127 return False
128 else:
129 return stat.S_ISREG(st.st_mode)
130
131
132 class LocalFactory:
133 """thin wrapper to dispatch between localrepo and bundle repo"""
134
135 @staticmethod
136 def islocal(path: bytes) -> bool:
137 path = util.expandpath(urlutil.urllocalpath(path))
138 return not _isfile(path)
139
140 @staticmethod
141 def instance(ui, path, *args, **kwargs):
142 path = util.expandpath(urlutil.urllocalpath(path))
143 if _isfile(path):
144 cls = bundlerepo
145 else:
146 cls = localrepo
147 return cls.instance(ui, path, *args, **kwargs)
148
149
150 repo_schemes = {
133 151 b'bundle': bundlerepo,
134 152 b'union': unionrepo,
135 b'file': _local,
153 b'file': LocalFactory,
154 }
155
156 peer_schemes = {
136 157 b'http': httppeer,
137 158 b'https': httppeer,
138 159 b'ssh': sshpeer,
@@ -140,27 +161,23 b' schemes = {'
140 161 }
141 162
142 163
143 def _peerlookup(path):
144 u = urlutil.url(path)
145 scheme = u.scheme or b'file'
146 thing = schemes.get(scheme) or schemes[b'file']
147 try:
148 return thing(path)
149 except TypeError:
150 # we can't test callable(thing) because 'thing' can be an unloaded
151 # module that implements __call__
152 if not util.safehasattr(thing, b'instance'):
153 raise
154 return thing
155
156
157 164 def islocal(repo):
158 165 '''return true if repo (or path pointing to repo) is local'''
159 166 if isinstance(repo, bytes):
160 try:
161 return _peerlookup(repo).islocal(repo)
162 except AttributeError:
163 return False
167 u = urlutil.url(repo)
168 scheme = u.scheme or b'file'
169 if scheme in peer_schemes:
170 cls = peer_schemes[scheme]
171 cls.make_peer # make sure we load the module
172 elif scheme in repo_schemes:
173 cls = repo_schemes[scheme]
174 cls.instance # make sure we load the module
175 else:
176 cls = LocalFactory
177 if util.safehasattr(cls, 'islocal'):
178 return cls.islocal(repo) # pytype: disable=module-attr
179 return False
180 repo.ui.deprecwarn(b"use obj.local() instead of islocal(obj)", b"6.4")
164 181 return repo.local()
165 182
166 183
@@ -177,13 +194,7 b' def openpath(ui, path, sendaccept=True):'
177 194 wirepeersetupfuncs = []
178 195
179 196
180 def _peerorrepo(
181 ui, path, create=False, presetupfuncs=None, intents=None, createopts=None
182 ):
183 """return a repository object for the specified path"""
184 obj = _peerlookup(path).instance(
185 ui, path, create, intents=intents, createopts=createopts
186 )
197 def _setup_repo_or_peer(ui, obj, presetupfuncs=None):
187 198 ui = getattr(obj, "ui", ui)
188 199 for f in presetupfuncs or []:
189 200 f(ui, obj)
@@ -195,14 +206,12 b' def _peerorrepo('
195 206 if hook:
196 207 with util.timedcm('reposetup %r', name) as stats:
197 208 hook(ui, obj)
198 ui.log(
199 b'extension', b' > reposetup for %s took %s\n', name, stats
200 )
209 msg = b' > reposetup for %s took %s\n'
210 ui.log(b'extension', msg, name, stats)
201 211 ui.log(b'extension', b'> all reposetup took %s\n', allreposetupstats)
202 212 if not obj.local():
203 213 for f in wirepeersetupfuncs:
204 214 f(ui, obj)
205 return obj
206 215
207 216
208 217 def repository(
@@ -214,28 +223,59 b' def repository('
214 223 createopts=None,
215 224 ):
216 225 """return a repository object for the specified path"""
217 peer = _peerorrepo(
226 scheme = urlutil.url(path).scheme
227 if scheme is None:
228 scheme = b'file'
229 cls = repo_schemes.get(scheme)
230 if cls is None:
231 if scheme in peer_schemes:
232 raise error.Abort(_(b"repository '%s' is not local") % path)
233 cls = LocalFactory
234 repo = cls.instance(
218 235 ui,
219 236 path,
220 237 create,
221 presetupfuncs=presetupfuncs,
222 238 intents=intents,
223 239 createopts=createopts,
224 240 )
225 repo = peer.local()
226 if not repo:
227 raise error.Abort(
228 _(b"repository '%s' is not local") % (path or peer.url())
229 )
241 _setup_repo_or_peer(ui, repo, presetupfuncs=presetupfuncs)
230 242 return repo.filtered(b'visible')
231 243
232 244
233 245 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
234 246 '''return a repository peer for the specified path'''
247 ui = getattr(uiorrepo, 'ui', uiorrepo)
235 248 rui = remoteui(uiorrepo, opts)
236 return _peerorrepo(
237 rui, path, create, intents=intents, createopts=createopts
238 ).peer()
249 if util.safehasattr(path, 'url'):
250 # this is already a urlutil.path object
251 peer_path = path
252 else:
253 peer_path = urlutil.path(ui, None, rawloc=path, validate_path=False)
254 scheme = peer_path.url.scheme # pytype: disable=attribute-error
255 if scheme in peer_schemes:
256 cls = peer_schemes[scheme]
257 peer = cls.make_peer(
258 rui,
259 peer_path,
260 create,
261 intents=intents,
262 createopts=createopts,
263 )
264 _setup_repo_or_peer(rui, peer)
265 else:
266 # this is a repository
267 repo_path = peer_path.loc # pytype: disable=attribute-error
268 if not repo_path:
269 repo_path = peer_path.rawloc # pytype: disable=attribute-error
270 repo = repository(
271 rui,
272 repo_path,
273 create,
274 intents=intents,
275 createopts=createopts,
276 )
277 peer = repo.peer(path=peer_path)
278 return peer
239 279
240 280
241 281 def defaultdest(source):
@@ -290,17 +330,23 b' def share('
290 330 ):
291 331 '''create a shared repository'''
292 332
293 if not islocal(source):
294 raise error.Abort(_(b'can only share local repositories'))
333 not_local_msg = _(b'can only share local repositories')
334 if util.safehasattr(source, 'local'):
335 if source.local() is None:
336 raise error.Abort(not_local_msg)
337 elif not islocal(source):
338 # XXX why are we getting bytes here ?
339 raise error.Abort(not_local_msg)
295 340
296 341 if not dest:
297 342 dest = defaultdest(source)
298 343 else:
299 dest = urlutil.get_clone_path(ui, dest)[1]
344 dest = urlutil.get_clone_path_obj(ui, dest).loc
300 345
301 346 if isinstance(source, bytes):
302 origsource, source, branches = urlutil.get_clone_path(ui, source)
303 srcrepo = repository(ui, source)
347 source_path = urlutil.get_clone_path_obj(ui, source)
348 srcrepo = repository(ui, source_path.loc)
349 branches = (source_path.branch, [])
304 350 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
305 351 else:
306 352 srcrepo = source.local()
@@ -661,12 +707,23 b' def clone('
661 707 """
662 708
663 709 if isinstance(source, bytes):
664 src = urlutil.get_clone_path(ui, source, branch)
665 origsource, source, branches = src
666 srcpeer = peer(ui, peeropts, source)
710 src_path = urlutil.get_clone_path_obj(ui, source)
711 if src_path is None:
712 srcpeer = peer(ui, peeropts, b'')
713 origsource = source = b''
714 branches = (None, branch or [])
715 else:
716 srcpeer = peer(ui, peeropts, src_path)
717 origsource = src_path.rawloc
718 branches = (src_path.branch, branch or [])
719 source = src_path.loc
667 720 else:
668 srcpeer = source.peer() # in case we were called with a localrepo
721 if util.safehasattr(source, 'peer'):
722 srcpeer = source.peer() # in case we were called with a localrepo
723 else:
724 srcpeer = source
669 725 branches = (None, branch or [])
726 # XXX path: simply use the peer `path` object when this become available
670 727 origsource = source = srcpeer.url()
671 728 srclock = destlock = destwlock = cleandir = None
672 729 destpeer = None
@@ -678,7 +735,11 b' def clone('
678 735 if dest:
679 736 ui.status(_(b"destination directory: %s\n") % dest)
680 737 else:
681 dest = urlutil.get_clone_path(ui, dest)[0]
738 dest_path = urlutil.get_clone_path_obj(ui, dest)
739 if dest_path is not None:
740 dest = dest_path.rawloc
741 else:
742 dest = b''
682 743
683 744 dest = urlutil.urllocalpath(dest)
684 745 source = urlutil.urllocalpath(source)
@@ -1271,23 +1332,28 b' def _incoming('
1271 1332 msg %= len(srcs)
1272 1333 raise error.Abort(msg)
1273 1334 path = srcs[0]
1274 source, branches = urlutil.parseurl(path.rawloc, opts.get(b'branch'))
1275 if subpath is not None:
1335 if subpath is None:
1336 peer_path = path
1337 url = path.loc
1338 else:
1339 # XXX path: we are losing the `path` object here. Keeping it would be
1340 # valuable. For example as a "variant" as we do for pushes.
1276 1341 subpath = urlutil.url(subpath)
1277 1342 if subpath.isabs():
1278 source = bytes(subpath)
1343 peer_path = url = bytes(subpath)
1279 1344 else:
1280 p = urlutil.url(source)
1345 p = urlutil.url(path.loc)
1281 1346 if p.islocal():
1282 1347 normpath = os.path.normpath
1283 1348 else:
1284 1349 normpath = posixpath.normpath
1285 1350 p.path = normpath(b'%s/%s' % (p.path, subpath))
1286 source = bytes(p)
1287 other = peer(repo, opts, source)
1351 peer_path = url = bytes(p)
1352 other = peer(repo, opts, peer_path)
1288 1353 cleanupfn = other.close
1289 1354 try:
1290 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(source))
1355 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(url))
1356 branches = (path.branch, opts.get(b'branch', []))
1291 1357 revs, checkout = addbranchrevs(repo, other, branches, opts.get(b'rev'))
1292 1358
1293 1359 if revs:
@@ -1346,7 +1412,7 b' def _outgoing(ui, repo, dests, opts, sub'
1346 1412 out = set()
1347 1413 others = []
1348 1414 for path in urlutil.get_push_paths(repo, ui, dests):
1349 dest = path.pushloc or path.loc
1415 dest = path.loc
1350 1416 if subpath is not None:
1351 1417 subpath = urlutil.url(subpath)
1352 1418 if subpath.isabs():
@@ -230,8 +230,9 b' class requestcontext:'
230 230
231 231 def sendtemplate(self, name, **kwargs):
232 232 """Helper function to send a response generated from a template."""
233 kwargs = pycompat.byteskwargs(kwargs)
234 self.res.setbodygen(self.tmpl.generate(name, kwargs))
233 if self.req.method != b'HEAD':
234 kwargs = pycompat.byteskwargs(kwargs)
235 self.res.setbodygen(self.tmpl.generate(name, kwargs))
235 236 return self.res.sendresponse()
236 237
237 238
@@ -485,6 +485,7 b' class wsgiresponse:'
485 485 self._bodybytes is None
486 486 and self._bodygen is None
487 487 and not self._bodywillwrite
488 and self._req.method != b'HEAD'
488 489 ):
489 490 raise error.ProgrammingError(b'response body not defined')
490 491
@@ -594,6 +595,8 b' class wsgiresponse:'
594 595 yield chunk
595 596 elif self._bodywillwrite:
596 597 self._bodywritefn = write
598 elif self._req.method == b'HEAD':
599 pass
597 600 else:
598 601 error.ProgrammingError(b'do not know how to send body')
599 602
@@ -151,6 +151,9 b' class _httprequesthandler(httpservermod.'
151 151 def do_GET(self):
152 152 self.do_POST()
153 153
154 def do_HEAD(self):
155 self.do_POST()
156
154 157 def do_hgweb(self):
155 158 self.sent_headers = False
156 159 path, query = _splitURI(self.path)
@@ -246,7 +249,11 b' class _httprequesthandler(httpservermod.'
246 249 self.send_header(*h)
247 250 if h[0].lower() == 'content-length':
248 251 self.length = int(h[1])
249 if self.length is None and saved_status[0] != common.HTTP_NOT_MODIFIED:
252 if (
253 self.length is None
254 and saved_status[0] != common.HTTP_NOT_MODIFIED
255 and self.command != 'HEAD'
256 ):
250 257 self._chunked = (
251 258 not self.close_connection and self.request_version == 'HTTP/1.1'
252 259 )
@@ -1299,6 +1299,9 b' def archive(web):'
1299 1299 b'sendresponse() should not emit data if writing later'
1300 1300 )
1301 1301
1302 if web.req.method == b'HEAD':
1303 return []
1304
1302 1305 bodyfh = web.res.getbodyfile()
1303 1306
1304 1307 archival.archive(
@@ -382,8 +382,7 b' def parsev1commandresponse(ui, baseurl, '
382 382
383 383 class httppeer(wireprotov1peer.wirepeer):
384 384 def __init__(self, ui, path, url, opener, requestbuilder, caps):
385 self.ui = ui
386 self._path = path
385 super().__init__(ui, path=path)
387 386 self._url = url
388 387 self._caps = caps
389 388 self.limitedarguments = caps is not None and b'httppostargs' not in caps
@@ -398,14 +397,11 b' class httppeer(wireprotov1peer.wirepeer)'
398 397 # Begin of ipeerconnection interface.
399 398
400 399 def url(self):
401 return self._path
400 return self.path.loc
402 401
403 402 def local(self):
404 403 return None
405 404
406 def peer(self):
407 return self
408
409 405 def canpush(self):
410 406 return True
411 407
@@ -605,14 +601,13 b' def makepeer(ui, path, opener=None, requ'
605 601 ``requestbuilder`` is the type used for constructing HTTP requests.
606 602 It exists as an argument so extensions can override the default.
607 603 """
608 u = urlutil.url(path)
609 if u.query or u.fragment:
610 raise error.Abort(
611 _(b'unsupported URL component: "%s"') % (u.query or u.fragment)
612 )
604 if path.url.query or path.url.fragment:
605 msg = _(b'unsupported URL component: "%s"')
606 msg %= path.url.query or path.url.fragment
607 raise error.Abort(msg)
613 608
614 609 # urllib cannot handle URLs with embedded user or passwd.
615 url, authinfo = u.authinfo()
610 url, authinfo = path.url.authinfo()
616 611 ui.debug(b'using %s\n' % url)
617 612
618 613 opener = opener or urlmod.opener(ui, authinfo)
@@ -624,11 +619,11 b' def makepeer(ui, path, opener=None, requ'
624 619 )
625 620
626 621
627 def instance(ui, path, create, intents=None, createopts=None):
622 def make_peer(ui, path, create, intents=None, createopts=None):
628 623 if create:
629 624 raise error.Abort(_(b'cannot create new http repository'))
630 625 try:
631 if path.startswith(b'https:') and not urlmod.has_https:
626 if path.url.scheme == b'https' and not urlmod.has_https:
632 627 raise error.Abort(
633 628 _(b'Python support for SSL and HTTPS is not installed')
634 629 )
@@ -638,7 +633,7 b' def instance(ui, path, create, intents=N'
638 633 return inst
639 634 except error.RepoError as httpexception:
640 635 try:
641 r = statichttprepo.instance(ui, b"static-" + path, create)
636 r = statichttprepo.make_peer(ui, b"static-" + path.loc, create)
642 637 ui.note(_(b'(falling back to static-http)\n'))
643 638 return r
644 639 except error.RepoError:
@@ -12,6 +12,7 b' class idirstate(interfaceutil.Interface)'
12 12 sparsematchfn,
13 13 nodeconstants,
14 14 use_dirstate_v2,
15 use_tracked_hint=False,
15 16 ):
16 17 """Create a new dirstate object.
17 18
@@ -23,6 +24,15 b' class idirstate(interfaceutil.Interface)'
23 24 # TODO: all these private methods and attributes should be made
24 25 # public or removed from the interface.
25 26 _ignore = interfaceutil.Attribute("""Matcher for ignored files.""")
27 is_changing_any = interfaceutil.Attribute(
28 """True if any changes in progress."""
29 )
30 is_changing_parents = interfaceutil.Attribute(
31 """True if parents changes in progress."""
32 )
33 is_changing_files = interfaceutil.Attribute(
34 """True if file tracking changes in progress."""
35 )
26 36
27 37 def _ignorefiles():
28 38 """Return a list of files containing patterns to ignore."""
@@ -34,7 +44,7 b' class idirstate(interfaceutil.Interface)'
34 44 _checkexec = interfaceutil.Attribute("""Callable for checking exec bits.""")
35 45
36 46 @contextlib.contextmanager
37 def parentchange():
47 def changing_parents(repo):
38 48 """Context manager for handling dirstate parents.
39 49
40 50 If an exception occurs in the scope of the context manager,
@@ -42,16 +52,26 b' class idirstate(interfaceutil.Interface)'
42 52 released.
43 53 """
44 54
45 def pendingparentchange():
46 """Returns true if the dirstate is in the middle of a set of changes
47 that modify the dirstate parent.
55 @contextlib.contextmanager
56 def changing_files(repo):
57 """Context manager for handling dirstate files.
58
59 If an exception occurs in the scope of the context manager,
60 the incoherent dirstate won't be written when wlock is
61 released.
48 62 """
49 63
50 64 def hasdir(d):
51 65 pass
52 66
53 67 def flagfunc(buildfallback):
54 pass
68 """build a callable that returns flags associated with a filename
69
70 The information is extracted from three possible layers:
71 1. the file system if it supports the information
72 2. the "fallback" information stored in the dirstate if any
73 3. a more expensive mechanism inferring the flags from the parents.
74 """
55 75
56 76 def getcwd():
57 77 """Return the path from which a canonical path is calculated.
@@ -61,12 +81,12 b' class idirstate(interfaceutil.Interface)'
61 81 used to get real file paths. Use vfs functions instead.
62 82 """
63 83
84 def pathto(f, cwd=None):
85 pass
86
64 87 def get_entry(path):
65 88 """return a DirstateItem for the associated path"""
66 89
67 def pathto(f, cwd=None):
68 pass
69
70 90 def __contains__(key):
71 91 """Check if bytestring `key` is known to the dirstate."""
72 92
@@ -96,7 +116,7 b' class idirstate(interfaceutil.Interface)'
96 116 def setparents(p1, p2=None):
97 117 """Set dirstate parents to p1 and p2.
98 118
99 When moving from two parents to one, 'm' merged entries a
119 When moving from two parents to one, "merged" entries a
100 120 adjusted to normal and previous copy records discarded and
101 121 returned by the call.
102 122
@@ -147,7 +167,7 b' class idirstate(interfaceutil.Interface)'
147 167 pass
148 168
149 169 def identity():
150 """Return identity of dirstate it to detect changing in storage
170 """Return identity of dirstate itself to detect changing in storage
151 171
152 172 If identity of previous dirstate is equal to this, writing
153 173 changes based on the former dirstate out can keep consistency.
@@ -200,11 +220,7 b' class idirstate(interfaceutil.Interface)'
200 220 return files in the dirstate (in whatever state) filtered by match
201 221 """
202 222
203 def savebackup(tr, backupname):
204 '''Save current dirstate into backup file'''
205
206 def restorebackup(tr, backupname):
207 '''Restore dirstate by backup file'''
208
209 def clearbackup(tr, backupname):
210 '''Clear backup file'''
223 def verify(m1, m2, p1, narrow_matcher=None):
224 """
225 check the dirstate contents against the parent manifest and yield errors
226 """
@@ -103,6 +103,7 b' class ipeerconnection(interfaceutil.Inte'
103 103 """
104 104
105 105 ui = interfaceutil.Attribute("""ui.ui instance""")
106 path = interfaceutil.Attribute("""a urlutil.path instance or None""")
106 107
107 108 def url():
108 109 """Returns a URL string representing this peer.
@@ -123,12 +124,6 b' class ipeerconnection(interfaceutil.Inte'
123 124 can be used to interface with it. Otherwise returns ``None``.
124 125 """
125 126
126 def peer():
127 """Returns an object conforming to this interface.
128
129 Most implementations will ``return self``.
130 """
131
132 127 def canpush():
133 128 """Returns a boolean indicating if this peer can be pushed to."""
134 129
@@ -393,6 +388,10 b' class peer:'
393 388
394 389 limitedarguments = False
395 390
391 def __init__(self, ui, path=None):
392 self.ui = ui
393 self.path = path
394
396 395 def capable(self, name):
397 396 caps = self.capabilities()
398 397 if name in caps:
@@ -1613,7 +1612,7 b' class ilocalrepositorymain(interfaceutil'
1613 1612 def close():
1614 1613 """Close the handle on this repository."""
1615 1614
1616 def peer():
1615 def peer(path=None):
1617 1616 """Obtain an object conforming to the ``peer`` interface."""
1618 1617
1619 1618 def unfiltered():
@@ -10,11 +10,16 b''
10 10 import functools
11 11 import os
12 12 import random
13 import re
13 14 import sys
14 15 import time
15 16 import weakref
16 17
17 18 from concurrent import futures
19 from typing import (
20 Optional,
21 )
22
18 23 from .i18n import _
19 24 from .node import (
20 25 bin,
@@ -37,7 +42,6 b' from . import ('
37 42 commit,
38 43 context,
39 44 dirstate,
40 dirstateguard,
41 45 discovery,
42 46 encoding,
43 47 error,
@@ -96,6 +100,8 b' release = lockmod.release'
96 100 urlerr = util.urlerr
97 101 urlreq = util.urlreq
98 102
103 RE_SKIP_DIRSTATE_ROLLBACK = re.compile(b"^(dirstate|narrowspec.dirstate).*")
104
99 105 # set of (path, vfs-location) tuples. vfs-location is:
100 106 # - 'plain for vfs relative paths
101 107 # - '' for svfs relative paths
@@ -299,13 +305,12 b' class localcommandexecutor:'
299 305 class localpeer(repository.peer):
300 306 '''peer for a local repo; reflects only the most recent API'''
301 307
302 def __init__(self, repo, caps=None):
303 super(localpeer, self).__init__()
308 def __init__(self, repo, caps=None, path=None):
309 super(localpeer, self).__init__(repo.ui, path=path)
304 310
305 311 if caps is None:
306 312 caps = moderncaps.copy()
307 313 self._repo = repo.filtered(b'served')
308 self.ui = repo.ui
309 314
310 315 if repo._wanted_sidedata:
311 316 formatted = bundle2.format_remote_wanted_sidedata(repo)
@@ -321,9 +326,6 b' class localpeer(repository.peer):'
321 326 def local(self):
322 327 return self._repo
323 328
324 def peer(self):
325 return self
326
327 329 def canpush(self):
328 330 return True
329 331
@@ -451,8 +453,8 b' class locallegacypeer(localpeer):'
451 453 """peer extension which implements legacy methods too; used for tests with
452 454 restricted capabilities"""
453 455
454 def __init__(self, repo):
455 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
456 def __init__(self, repo, path=None):
457 super(locallegacypeer, self).__init__(repo, caps=legacycaps, path=path)
456 458
457 459 # Begin of baselegacywirecommands interface.
458 460
@@ -526,7 +528,7 b' def _readrequires(vfs, allowmissing):'
526 528 return set(read(b'requires').splitlines())
527 529
528 530
529 def makelocalrepository(baseui, path, intents=None):
531 def makelocalrepository(baseui, path: bytes, intents=None):
530 532 """Create a local repository object.
531 533
532 534 Given arguments needed to construct a local repository, this function
@@ -612,7 +614,6 b' def makelocalrepository(baseui, path, in'
612 614 # to be reshared
613 615 hint = _(b"see `hg help config.format.use-share-safe` for more information")
614 616 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
615
616 617 if (
617 618 shared
618 619 and requirementsmod.SHARESAFE_REQUIREMENT
@@ -845,7 +846,13 b' def makelocalrepository(baseui, path, in'
845 846 )
846 847
847 848
848 def loadhgrc(ui, wdirvfs, hgvfs, requirements, sharedvfs=None):
849 def loadhgrc(
850 ui,
851 wdirvfs: vfsmod.vfs,
852 hgvfs: vfsmod.vfs,
853 requirements,
854 sharedvfs: Optional[vfsmod.vfs] = None,
855 ):
849 856 """Load hgrc files/content into a ui instance.
850 857
851 858 This is called during repository opening to load any additional
@@ -1058,6 +1065,8 b' def resolverevlogstorevfsoptions(ui, req'
1058 1065 options[b'revlogv2'] = True
1059 1066 if requirementsmod.CHANGELOGV2_REQUIREMENT in requirements:
1060 1067 options[b'changelogv2'] = True
1068 cmp_rank = ui.configbool(b'experimental', b'changelog-v2.compute-rank')
1069 options[b'changelogv2.compute-rank'] = cmp_rank
1061 1070
1062 1071 if requirementsmod.GENERALDELTA_REQUIREMENT in requirements:
1063 1072 options[b'generaldelta'] = True
@@ -1071,6 +1080,11 b' def resolverevlogstorevfsoptions(ui, req'
1071 1080 b'storage', b'revlog.optimize-delta-parent-choice'
1072 1081 )
1073 1082 options[b'deltabothparents'] = deltabothparents
1083 dps_cgds = ui.configint(
1084 b'storage',
1085 b'revlog.delta-parent-search.candidate-group-chunk-size',
1086 )
1087 options[b'delta-parent-search.candidate-group-chunk-size'] = dps_cgds
1074 1088 options[b'debug-delta'] = ui.configbool(b'debug', b'revlog.debug-delta')
1075 1089
1076 1090 issue6528 = ui.configbool(b'storage', b'revlog.issue6528.fix-incoming')
@@ -1311,8 +1325,6 b' class localrepository:'
1311 1325 # XXX cache is a complicatged business someone
1312 1326 # should investigate this in depth at some point
1313 1327 b'cache/',
1314 # XXX shouldn't be dirstate covered by the wlock?
1315 b'dirstate',
1316 1328 # XXX bisect was still a bit too messy at the time
1317 1329 # this changeset was introduced. Someone should fix
1318 1330 # the remainig bit and drop this line
@@ -1323,15 +1335,15 b' class localrepository:'
1323 1335 self,
1324 1336 baseui,
1325 1337 ui,
1326 origroot,
1327 wdirvfs,
1328 hgvfs,
1338 origroot: bytes,
1339 wdirvfs: vfsmod.vfs,
1340 hgvfs: vfsmod.vfs,
1329 1341 requirements,
1330 1342 supportedrequirements,
1331 sharedpath,
1343 sharedpath: bytes,
1332 1344 store,
1333 cachevfs,
1334 wcachevfs,
1345 cachevfs: vfsmod.vfs,
1346 wcachevfs: vfsmod.vfs,
1335 1347 features,
1336 1348 intents=None,
1337 1349 ):
@@ -1453,6 +1465,7 b' class localrepository:'
1453 1465 # - bookmark changes
1454 1466 self.filteredrevcache = {}
1455 1467
1468 self._dirstate = None
1456 1469 # post-dirstate-status hooks
1457 1470 self._postdsstatus = []
1458 1471
@@ -1620,8 +1633,8 b' class localrepository:'
1620 1633 parts.pop()
1621 1634 return False
1622 1635
1623 def peer(self):
1624 return localpeer(self) # not cached to avoid reference cycle
1636 def peer(self, path=None):
1637 return localpeer(self, path=path) # not cached to avoid reference cycle
1625 1638
1626 1639 def unfiltered(self):
1627 1640 """Return unfiltered version of the repository
@@ -1738,9 +1751,13 b' class localrepository:'
1738 1751 def manifestlog(self):
1739 1752 return self.store.manifestlog(self, self._storenarrowmatch)
1740 1753
1741 @repofilecache(b'dirstate')
1754 @unfilteredpropertycache
1742 1755 def dirstate(self):
1743 return self._makedirstate()
1756 if self._dirstate is None:
1757 self._dirstate = self._makedirstate()
1758 else:
1759 self._dirstate.refresh()
1760 return self._dirstate
1744 1761
1745 1762 def _makedirstate(self):
1746 1763 """Extension point for wrapping the dirstate per-repo."""
@@ -1977,7 +1994,7 b' class localrepository:'
1977 1994 def __iter__(self):
1978 1995 return iter(self.changelog)
1979 1996
1980 def revs(self, expr, *args):
1997 def revs(self, expr: bytes, *args):
1981 1998 """Find revisions matching a revset.
1982 1999
1983 2000 The revset is specified as a string ``expr`` that may contain
@@ -1993,7 +2010,7 b' class localrepository:'
1993 2010 tree = revsetlang.spectree(expr, *args)
1994 2011 return revset.makematcher(tree)(self)
1995 2012
1996 def set(self, expr, *args):
2013 def set(self, expr: bytes, *args):
1997 2014 """Find revisions matching a revset and emit changectx instances.
1998 2015
1999 2016 This is a convenience wrapper around ``revs()`` that iterates the
@@ -2005,7 +2022,7 b' class localrepository:'
2005 2022 for r in self.revs(expr, *args):
2006 2023 yield self[r]
2007 2024
2008 def anyrevs(self, specs, user=False, localalias=None):
2025 def anyrevs(self, specs: bytes, user=False, localalias=None):
2009 2026 """Find revisions matching one of the given revsets.
2010 2027
2011 2028 Revset aliases from the configuration are not expanded by default. To
@@ -2030,7 +2047,7 b' class localrepository:'
2030 2047 m = revset.matchany(None, specs, localalias=localalias)
2031 2048 return m(self)
2032 2049
2033 def url(self):
2050 def url(self) -> bytes:
2034 2051 return b'file:' + self.root
2035 2052
2036 2053 def hook(self, name, throw=False, **args):
@@ -2108,7 +2125,7 b' class localrepository:'
2108 2125 # writing to the cache), but the rest of Mercurial wants them in
2109 2126 # local encoding.
2110 2127 tags = {}
2111 for (name, (node, hist)) in alltags.items():
2128 for name, (node, hist) in alltags.items():
2112 2129 if node != self.nullid:
2113 2130 tags[encoding.tolocal(name)] = node
2114 2131 tags[b'tip'] = self.changelog.tip()
@@ -2229,7 +2246,7 b' class localrepository:'
2229 2246 return b'store'
2230 2247 return None
2231 2248
2232 def wjoin(self, f, *insidef):
2249 def wjoin(self, f: bytes, *insidef: bytes) -> bytes:
2233 2250 return self.vfs.reljoin(self.root, f, *insidef)
2234 2251
2235 2252 def setparents(self, p1, p2=None):
@@ -2238,17 +2255,17 b' class localrepository:'
2238 2255 self[None].setparents(p1, p2)
2239 2256 self._quick_access_changeid_invalidate()
2240 2257
2241 def filectx(self, path, changeid=None, fileid=None, changectx=None):
2258 def filectx(self, path: bytes, changeid=None, fileid=None, changectx=None):
2242 2259 """changeid must be a changeset revision, if specified.
2243 2260 fileid can be a file revision or node."""
2244 2261 return context.filectx(
2245 2262 self, path, changeid, fileid, changectx=changectx
2246 2263 )
2247 2264
2248 def getcwd(self):
2265 def getcwd(self) -> bytes:
2249 2266 return self.dirstate.getcwd()
2250 2267
2251 def pathto(self, f, cwd=None):
2268 def pathto(self, f: bytes, cwd: Optional[bytes] = None) -> bytes:
2252 2269 return self.dirstate.pathto(f, cwd)
2253 2270
2254 2271 def _loadfilter(self, filter):
@@ -2300,14 +2317,21 b' class localrepository:'
2300 2317 def adddatafilter(self, name, filter):
2301 2318 self._datafilters[name] = filter
2302 2319
2303 def wread(self, filename):
2320 def wread(self, filename: bytes) -> bytes:
2304 2321 if self.wvfs.islink(filename):
2305 2322 data = self.wvfs.readlink(filename)
2306 2323 else:
2307 2324 data = self.wvfs.read(filename)
2308 2325 return self._filter(self._encodefilterpats, filename, data)
2309 2326
2310 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
2327 def wwrite(
2328 self,
2329 filename: bytes,
2330 data: bytes,
2331 flags: bytes,
2332 backgroundclose=False,
2333 **kwargs
2334 ) -> int:
2311 2335 """write ``data`` into ``filename`` in the working directory
2312 2336
2313 2337 This returns length of written (maybe decoded) data.
@@ -2325,7 +2349,7 b' class localrepository:'
2325 2349 self.wvfs.setflags(filename, False, False)
2326 2350 return len(data)
2327 2351
2328 def wwritedata(self, filename, data):
2352 def wwritedata(self, filename: bytes, data: bytes) -> bytes:
2329 2353 return self._filter(self._decodefilterpats, filename, data)
2330 2354
2331 2355 def currenttransaction(self):
@@ -2356,6 +2380,21 b' class localrepository:'
2356 2380 hint=_(b"run 'hg recover' to clean up transaction"),
2357 2381 )
2358 2382
2383 # At that point your dirstate should be clean:
2384 #
2385 # - If you don't have the wlock, why would you still have a dirty
2386 # dirstate ?
2387 #
2388 # - If you hold the wlock, you should not be opening a transaction in
2389 # the middle of a `distate.changing_*` block. The transaction needs to
2390 # be open before that and wrap the change-context.
2391 #
2392 # - If you are not within a `dirstate.changing_*` context, why is our
2393 # dirstate dirty?
2394 if self.dirstate._dirty:
2395 m = "cannot open a transaction with a dirty dirstate"
2396 raise error.ProgrammingError(m)
2397
2359 2398 idbase = b"%.40f#%f" % (random.random(), time.time())
2360 2399 ha = hex(hashutil.sha1(idbase).digest())
2361 2400 txnid = b'TXN:' + ha
@@ -2514,7 +2553,6 b' class localrepository:'
2514 2553 # out) in this transaction
2515 2554 narrowspec.restorebackup(self, b'journal.narrowspec')
2516 2555 narrowspec.restorewcbackup(self, b'journal.narrowspec.dirstate')
2517 repo.dirstate.restorebackup(None, b'journal.dirstate')
2518 2556
2519 2557 repo.invalidate(clearfilecache=True)
2520 2558
@@ -2612,33 +2650,50 b' class localrepository:'
2612 2650 tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats)
2613 2651 self._transref = weakref.ref(tr)
2614 2652 scmutil.registersummarycallback(self, tr, desc)
2653 # This only exist to deal with the need of rollback to have viable
2654 # parents at the end of the operation. So backup viable parents at the
2655 # time of this operation.
2656 #
2657 # We only do it when the `wlock` is taken, otherwise other might be
2658 # altering the dirstate under us.
2659 #
2660 # This is really not a great way to do this (first, because we cannot
2661 # always do it). There are more viable alternative that exists
2662 #
2663 # - backing only the working copy parent in a dedicated files and doing
2664 # a clean "keep-update" to them on `hg rollback`.
2665 #
2666 # - slightly changing the behavior an applying a logic similar to "hg
2667 # strip" to pick a working copy destination on `hg rollback`
2668 if self.currentwlock() is not None:
2669 ds = self.dirstate
2670
2671 def backup_dirstate(tr):
2672 for f in ds.all_file_names():
2673 # hardlink backup is okay because `dirstate` is always
2674 # atomically written and possible data file are append only
2675 # and resistant to trailing data.
2676 tr.addbackup(f, hardlink=True, location=b'plain')
2677
2678 tr.addvalidator(b'dirstate-backup', backup_dirstate)
2615 2679 return tr
2616 2680
2617 2681 def _journalfiles(self):
2618 first = (
2682 return (
2619 2683 (self.svfs, b'journal'),
2620 2684 (self.svfs, b'journal.narrowspec'),
2621 2685 (self.vfs, b'journal.narrowspec.dirstate'),
2622 (self.vfs, b'journal.dirstate'),
2623 )
2624 middle = []
2625 dirstate_data = self.dirstate.data_backup_filename(b'journal.dirstate')
2626 if dirstate_data is not None:
2627 middle.append((self.vfs, dirstate_data))
2628 end = (
2629 2686 (self.vfs, b'journal.branch'),
2630 2687 (self.vfs, b'journal.desc'),
2631 2688 (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
2632 2689 (self.svfs, b'journal.phaseroots'),
2633 2690 )
2634 return first + tuple(middle) + end
2635 2691
2636 2692 def undofiles(self):
2637 2693 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2638 2694
2639 2695 @unfilteredmethod
2640 2696 def _writejournal(self, desc):
2641 self.dirstate.savebackup(None, b'journal.dirstate')
2642 2697 narrowspec.savewcbackup(self, b'journal.narrowspec.dirstate')
2643 2698 narrowspec.savebackup(self, b'journal.narrowspec')
2644 2699 self.vfs.write(
@@ -2673,23 +2728,23 b' class localrepository:'
2673 2728 return False
2674 2729
2675 2730 def rollback(self, dryrun=False, force=False):
2676 wlock = lock = dsguard = None
2731 wlock = lock = None
2677 2732 try:
2678 2733 wlock = self.wlock()
2679 2734 lock = self.lock()
2680 2735 if self.svfs.exists(b"undo"):
2681 dsguard = dirstateguard.dirstateguard(self, b'rollback')
2682
2683 return self._rollback(dryrun, force, dsguard)
2736 return self._rollback(dryrun, force)
2684 2737 else:
2685 2738 self.ui.warn(_(b"no rollback information available\n"))
2686 2739 return 1
2687 2740 finally:
2688 release(dsguard, lock, wlock)
2741 release(lock, wlock)
2689 2742
2690 2743 @unfilteredmethod # Until we get smarter cache management
2691 def _rollback(self, dryrun, force, dsguard):
2744 def _rollback(self, dryrun, force):
2692 2745 ui = self.ui
2746
2747 parents = self.dirstate.parents()
2693 2748 try:
2694 2749 args = self.vfs.read(b'undo.desc').splitlines()
2695 2750 (oldlen, desc, detail) = (int(args[0]), args[1], None)
@@ -2706,9 +2761,11 b' class localrepository:'
2706 2761 msg = _(
2707 2762 b'repository tip rolled back to revision %d (undo %s)\n'
2708 2763 ) % (oldtip, desc)
2764 parentgone = any(self[p].rev() > oldtip for p in parents)
2709 2765 except IOError:
2710 2766 msg = _(b'rolling back unknown transaction\n')
2711 2767 desc = None
2768 parentgone = True
2712 2769
2713 2770 if not force and self[b'.'] != self[b'tip'] and desc == b'commit':
2714 2771 raise error.Abort(
@@ -2723,11 +2780,18 b' class localrepository:'
2723 2780 if dryrun:
2724 2781 return 0
2725 2782
2726 parents = self.dirstate.parents()
2727 2783 self.destroying()
2728 2784 vfsmap = {b'plain': self.vfs, b'': self.svfs}
2785 skip_journal_pattern = None
2786 if not parentgone:
2787 skip_journal_pattern = RE_SKIP_DIRSTATE_ROLLBACK
2729 2788 transaction.rollback(
2730 self.svfs, vfsmap, b'undo', ui.warn, checkambigfiles=_cachedfiles
2789 self.svfs,
2790 vfsmap,
2791 b'undo',
2792 ui.warn,
2793 checkambigfiles=_cachedfiles,
2794 skip_journal_pattern=skip_journal_pattern,
2731 2795 )
2732 2796 bookmarksvfs = bookmarks.bookmarksvfs(self)
2733 2797 if bookmarksvfs.exists(b'undo.bookmarks'):
@@ -2737,16 +2801,20 b' class localrepository:'
2737 2801 if self.svfs.exists(b'undo.phaseroots'):
2738 2802 self.svfs.rename(b'undo.phaseroots', b'phaseroots', checkambig=True)
2739 2803 self.invalidate()
2740
2741 has_node = self.changelog.index.has_node
2742 parentgone = any(not has_node(p) for p in parents)
2804 self.dirstate.invalidate()
2805
2743 2806 if parentgone:
2744 # prevent dirstateguard from overwriting already restored one
2745 dsguard.close()
2807 # replace this with some explicit parent update in the future.
2808 has_node = self.changelog.index.has_node
2809 if not all(has_node(p) for p in self.dirstate._pl):
2810 # There was no dirstate to backup initially, we need to drop
2811 # the existing one.
2812 with self.dirstate.changing_parents(self):
2813 self.dirstate.setparents(self.nullid)
2814 self.dirstate.clear()
2746 2815
2747 2816 narrowspec.restorebackup(self, b'undo.narrowspec')
2748 2817 narrowspec.restorewcbackup(self, b'undo.narrowspec.dirstate')
2749 self.dirstate.restorebackup(None, b'undo.dirstate')
2750 2818 try:
2751 2819 branch = self.vfs.read(b'undo.branch')
2752 2820 self.dirstate.setbranch(encoding.tolocal(branch))
@@ -2880,7 +2948,6 b' class localrepository:'
2880 2948 filtered.branchmap().write(filtered)
2881 2949
2882 2950 def invalidatecaches(self):
2883
2884 2951 if '_tagscache' in vars(self):
2885 2952 # can't use delattr on proxy
2886 2953 del self.__dict__['_tagscache']
@@ -2903,13 +2970,9 b' class localrepository:'
2903 2970 rereads the dirstate. Use dirstate.invalidate() if you want to
2904 2971 explicitly read the dirstate again (i.e. restoring it to a previous
2905 2972 known good state)."""
2906 if hasunfilteredcache(self, 'dirstate'):
2907 for k in self.dirstate._filecache:
2908 try:
2909 delattr(self.dirstate, k)
2910 except AttributeError:
2911 pass
2912 delattr(self.unfiltered(), 'dirstate')
2973 unfi = self.unfiltered()
2974 if 'dirstate' in unfi.__dict__:
2975 del unfi.__dict__['dirstate']
2913 2976
2914 2977 def invalidate(self, clearfilecache=False):
2915 2978 """Invalidates both store and non-store parts other than dirstate
@@ -2921,9 +2984,6 b' class localrepository:'
2921 2984 """
2922 2985 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2923 2986 for k in list(self._filecache.keys()):
2924 # dirstate is invalidated separately in invalidatedirstate()
2925 if k == b'dirstate':
2926 continue
2927 2987 if (
2928 2988 k == b'changelog'
2929 2989 and self.currenttransaction()
@@ -3052,12 +3112,19 b' class localrepository:'
3052 3112 self.ui.develwarn(b'"wlock" acquired after "lock"')
3053 3113
3054 3114 def unlock():
3055 if self.dirstate.pendingparentchange():
3115 if self.dirstate.is_changing_any:
3116 msg = b"wlock release in the middle of a changing parents"
3117 self.ui.develwarn(msg)
3056 3118 self.dirstate.invalidate()
3057 3119 else:
3120 if self.dirstate._dirty:
3121 msg = b"dirty dirstate on wlock release"
3122 self.ui.develwarn(msg)
3058 3123 self.dirstate.write(None)
3059 3124
3060 self._filecache[b'dirstate'].refresh()
3125 unfi = self.unfiltered()
3126 if 'dirstate' in unfi.__dict__:
3127 del unfi.__dict__['dirstate']
3061 3128
3062 3129 l = self._lock(
3063 3130 self.vfs,
@@ -3520,14 +3587,13 b' def aftertrans(files):'
3520 3587 return a
3521 3588
3522 3589
3523 def undoname(fn):
3590 def undoname(fn: bytes) -> bytes:
3524 3591 base, name = os.path.split(fn)
3525 3592 assert name.startswith(b'journal')
3526 3593 return os.path.join(base, name.replace(b'journal', b'undo', 1))
3527 3594
3528 3595
3529 def instance(ui, path, create, intents=None, createopts=None):
3530
3596 def instance(ui, path: bytes, create, intents=None, createopts=None):
3531 3597 # prevent cyclic import localrepo -> upgrade -> localrepo
3532 3598 from . import upgrade
3533 3599
@@ -3543,7 +3609,7 b' def instance(ui, path, create, intents=N'
3543 3609 return repo
3544 3610
3545 3611
3546 def islocal(path):
3612 def islocal(path: bytes) -> bool:
3547 3613 return True
3548 3614
3549 3615
@@ -3803,7 +3869,7 b' def filterknowncreateopts(ui, createopts'
3803 3869 return {k: v for k, v in createopts.items() if k not in known}
3804 3870
3805 3871
3806 def createrepository(ui, path, createopts=None, requirements=None):
3872 def createrepository(ui, path: bytes, createopts=None, requirements=None):
3807 3873 """Create a new repository in a vfs.
3808 3874
3809 3875 ``path`` path to the new repo's working directory.
@@ -113,7 +113,7 b' def activepath(repo, remote):'
113 113 if local:
114 114 rpath = util.pconvert(remote._repo.root)
115 115 elif not isinstance(remote, bytes):
116 rpath = remote._url
116 rpath = remote.url()
117 117
118 118 # represent the remotepath with user defined path name if exists
119 119 for path, url in repo.ui.configitems(b'paths'):
@@ -1836,6 +1836,7 b' class manifestrevlog:'
1836 1836 assumehaveparentrevisions=False,
1837 1837 deltamode=repository.CG_DELTAMODE_STD,
1838 1838 sidedata_helpers=None,
1839 debug_info=None,
1839 1840 ):
1840 1841 return self._revlog.emitrevisions(
1841 1842 nodes,
@@ -1844,6 +1845,7 b' class manifestrevlog:'
1844 1845 assumehaveparentrevisions=assumehaveparentrevisions,
1845 1846 deltamode=deltamode,
1846 1847 sidedata_helpers=sidedata_helpers,
1848 debug_info=debug_info,
1847 1849 )
1848 1850
1849 1851 def addgroup(
@@ -1854,6 +1856,8 b' class manifestrevlog:'
1854 1856 alwayscache=False,
1855 1857 addrevisioncb=None,
1856 1858 duplicaterevisioncb=None,
1859 debug_info=None,
1860 delta_base_reuse_policy=None,
1857 1861 ):
1858 1862 return self._revlog.addgroup(
1859 1863 deltas,
@@ -1862,6 +1866,8 b' class manifestrevlog:'
1862 1866 alwayscache=alwayscache,
1863 1867 addrevisioncb=addrevisioncb,
1864 1868 duplicaterevisioncb=duplicaterevisioncb,
1869 debug_info=debug_info,
1870 delta_base_reuse_policy=delta_base_reuse_policy,
1865 1871 )
1866 1872
1867 1873 def rawsize(self, rev):
@@ -368,7 +368,7 b' def _donormalize(patterns, default, root'
368 368 % (
369 369 pat,
370 370 inst.message,
371 ) # pytype: disable=unsupported-operands
371 )
372 372 )
373 373 except IOError as inst:
374 374 if warn:
@@ -94,6 +94,13 b' class diffopts:'
94 94 opts.update(kwargs)
95 95 return diffopts(**opts)
96 96
97 def __bytes__(self):
98 return b", ".join(
99 b"%s: %r" % (k, getattr(self, k)) for k in self.defaults
100 )
101
102 __str__ = encoding.strmethod(__bytes__)
103
97 104
98 105 defaultopts = diffopts()
99 106
@@ -46,7 +46,7 b' def _getcheckunknownconfig(repo, section'
46 46 return config
47 47
48 48
49 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
49 def _checkunknownfile(dirstate, wvfs, dircache, wctx, mctx, f, f2=None):
50 50 if wctx.isinmemory():
51 51 # Nothing to do in IMM because nothing in the "working copy" can be an
52 52 # unknown file.
@@ -58,9 +58,8 b' def _checkunknownfile(repo, wctx, mctx, '
58 58 if f2 is None:
59 59 f2 = f
60 60 return (
61 repo.wvfs.audit.check(f)
62 and repo.wvfs.isfileorlink(f)
63 and repo.dirstate.normalize(f) not in repo.dirstate
61 wvfs.isfileorlink_checkdir(dircache, f)
62 and dirstate.normalize(f) not in dirstate
64 63 and mctx[f2].cmp(wctx[f])
65 64 )
66 65
@@ -136,6 +135,9 b' def _checkunknownfiles(repo, wctx, mctx,'
136 135 pathconfig = repo.ui.configbool(
137 136 b'experimental', b'merge.checkpathconflicts'
138 137 )
138 dircache = dict()
139 dirstate = repo.dirstate
140 wvfs = repo.wvfs
139 141 if not force:
140 142
141 143 def collectconflicts(conflicts, config):
@@ -151,7 +153,7 b' def _checkunknownfiles(repo, wctx, mctx,'
151 153 mergestatemod.ACTION_DELETED_CHANGED,
152 154 )
153 155 ):
154 if _checkunknownfile(repo, wctx, mctx, f):
156 if _checkunknownfile(dirstate, wvfs, dircache, wctx, mctx, f):
155 157 fileconflicts.add(f)
156 158 elif pathconfig and f not in wctx:
157 159 path = checkunknowndirs(repo, wctx, f)
@@ -160,7 +162,9 b' def _checkunknownfiles(repo, wctx, mctx,'
160 162 for f, args, msg in mresult.getactions(
161 163 [mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]
162 164 ):
163 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
165 if _checkunknownfile(
166 dirstate, wvfs, dircache, wctx, mctx, f, args[0]
167 ):
164 168 fileconflicts.add(f)
165 169
166 170 allconflicts = fileconflicts | pathconflicts
@@ -173,7 +177,9 b' def _checkunknownfiles(repo, wctx, mctx,'
173 177 mresult.getactions([mergestatemod.ACTION_CREATED_MERGE])
174 178 ):
175 179 fl2, anc = args
176 different = _checkunknownfile(repo, wctx, mctx, f)
180 different = _checkunknownfile(
181 dirstate, wvfs, dircache, wctx, mctx, f
182 )
177 183 if repo.dirstate._ignore(f):
178 184 config = ignoredconfig
179 185 else:
@@ -240,16 +246,21 b' def _checkunknownfiles(repo, wctx, mctx,'
240 246 else:
241 247 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
242 248
243 for f, args, msg in list(
244 mresult.getactions([mergestatemod.ACTION_CREATED])
245 ):
249 def transformargs(f, args):
246 250 backup = (
247 251 f in fileconflicts
248 or f in pathconflicts
249 or any(p in pathconflicts for p in pathutil.finddirs(f))
252 or pathconflicts
253 and (
254 f in pathconflicts
255 or any(p in pathconflicts for p in pathutil.finddirs(f))
256 )
250 257 )
251 258 (flags,) = args
252 mresult.addfile(f, mergestatemod.ACTION_GET, (flags, backup), msg)
259 return (flags, backup)
260
261 mresult.mapaction(
262 mergestatemod.ACTION_CREATED, mergestatemod.ACTION_GET, transformargs
263 )
253 264
254 265
255 266 def _forgetremoved(wctx, mctx, branchmerge, mresult):
@@ -581,6 +592,18 b' class mergeresult:'
581 592 self._filemapping[filename] = (action, data, message)
582 593 self._actionmapping[action][filename] = (data, message)
583 594
595 def mapaction(self, actionfrom, actionto, transform):
596 """changes all occurrences of action `actionfrom` into `actionto`,
597 transforming its args with the function `transform`.
598 """
599 orig = self._actionmapping[actionfrom]
600 del self._actionmapping[actionfrom]
601 dest = self._actionmapping[actionto]
602 for f, (data, msg) in orig.items():
603 data = transform(f, data)
604 self._filemapping[f] = (actionto, data, msg)
605 dest[f] = (data, msg)
606
584 607 def getfile(self, filename, default_return=None):
585 608 """returns (action, args, msg) about this file
586 609
@@ -1142,6 +1165,8 b' def calculateupdates('
1142 1165 followcopies,
1143 1166 )
1144 1167 _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce)
1168 if repo.ui.configbool(b'devel', b'debug.abort-update'):
1169 exit(1)
1145 1170
1146 1171 else: # only when merge.preferancestor=* - the default
1147 1172 repo.ui.note(
@@ -2130,7 +2155,7 b' def _update('
2130 2155 assert len(getfiledata) == (
2131 2156 mresult.len((mergestatemod.ACTION_GET,)) if wantfiledata else 0
2132 2157 )
2133 with repo.dirstate.parentchange():
2158 with repo.dirstate.changing_parents(repo):
2134 2159 ### Filter Filedata
2135 2160 #
2136 2161 # We gathered "cache" information for the clean file while
@@ -2352,7 +2377,7 b' def graft('
2352 2377 # fix up dirstate for copies and renames
2353 2378 copies.graftcopies(wctx, ctx, base)
2354 2379 else:
2355 with repo.dirstate.parentchange():
2380 with repo.dirstate.changing_parents(repo):
2356 2381 repo.setparents(pctx.node(), pother)
2357 2382 repo.dirstate.write(repo.currenttransaction())
2358 2383 # fix up dirstate for copies and renames
@@ -322,10 +322,16 b' def updateworkingcopy(repo, assumeclean='
322 322 addedmatch = matchmod.differencematcher(newmatch, oldmatch)
323 323 removedmatch = matchmod.differencematcher(oldmatch, newmatch)
324 324
325 assert repo.currentwlock() is not None
325 326 ds = repo.dirstate
326 lookup, status, _mtime_boundary = ds.status(
327 removedmatch, subrepos=[], ignored=True, clean=True, unknown=True
328 )
327 with ds.running_status(repo):
328 lookup, status, _mtime_boundary = ds.status(
329 removedmatch,
330 subrepos=[],
331 ignored=True,
332 clean=True,
333 unknown=True,
334 )
329 335 trackeddirty = status.modified + status.added
330 336 clean = status.clean
331 337 if assumeclean:
@@ -570,22 +570,23 b' class workingbackend(fsbackend):'
570 570 self.changed.add(fname)
571 571
572 572 def close(self):
573 wctx = self.repo[None]
574 changed = set(self.changed)
575 for src, dst in self.copied:
576 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
577 if self.removed:
578 wctx.forget(sorted(self.removed))
579 for f in self.removed:
580 if f not in self.repo.dirstate:
581 # File was deleted and no longer belongs to the
582 # dirstate, it was probably marked added then
583 # deleted, and should not be considered by
584 # marktouched().
585 changed.discard(f)
586 if changed:
587 scmutil.marktouched(self.repo, changed, self.similarity)
588 return sorted(self.changed)
573 with self.repo.dirstate.changing_files(self.repo):
574 wctx = self.repo[None]
575 changed = set(self.changed)
576 for src, dst in self.copied:
577 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
578 if self.removed:
579 wctx.forget(sorted(self.removed))
580 for f in self.removed:
581 if f not in self.repo.dirstate:
582 # File was deleted and no longer belongs to the
583 # dirstate, it was probably marked added then
584 # deleted, and should not be considered by
585 # marktouched().
586 changed.discard(f)
587 if changed:
588 scmutil.marktouched(self.repo, changed, self.similarity)
589 return sorted(self.changed)
589 590
590 591
591 592 class filestore:
@@ -4,6 +4,13 b' import os'
4 4 import posixpath
5 5 import stat
6 6
7 from typing import (
8 Any,
9 Callable,
10 Iterator,
11 Optional,
12 )
13
7 14 from .i18n import _
8 15 from . import (
9 16 encoding,
@@ -13,15 +20,6 b' from . import ('
13 20 util,
14 21 )
15 22
16 if pycompat.TYPE_CHECKING:
17 from typing import (
18 Any,
19 Callable,
20 Iterator,
21 Optional,
22 )
23
24
25 23 rustdirs = policy.importrust('dirstate', 'Dirs')
26 24 parsers = policy.importmod('parsers')
27 25
@@ -56,7 +54,7 b' class pathauditor:'
56 54
57 55 def __init__(self, root, callback=None, realfs=True, cached=False):
58 56 self.audited = set()
59 self.auditeddir = set()
57 self.auditeddir = dict()
60 58 self.root = root
61 59 self._realfs = realfs
62 60 self._cached = cached
@@ -72,8 +70,7 b' class pathauditor:'
72 70 path may contain a pattern (e.g. foodir/**.txt)"""
73 71
74 72 path = util.localpath(path)
75 normpath = self.normcase(path)
76 if normpath in self.audited:
73 if path in self.audited:
77 74 return
78 75 # AIX ignores "/" at end of path, others raise EISDIR.
79 76 if util.endswithsep(path):
@@ -90,13 +87,14 b' class pathauditor:'
90 87 _(b"path contains illegal component: %s") % path
91 88 )
92 89 # Windows shortname aliases
93 for p in parts:
94 if b"~" in p:
95 first, last = p.split(b"~", 1)
96 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
97 raise error.InputError(
98 _(b"path contains illegal component: %s") % path
99 )
90 if b"~" in path:
91 for p in parts:
92 if b"~" in p:
93 first, last = p.split(b"~", 1)
94 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
95 raise error.InputError(
96 _(b"path contains illegal component: %s") % path
97 )
100 98 if b'.hg' in _lowerclean(path):
101 99 lparts = [_lowerclean(p) for p in parts]
102 100 for p in b'.hg', b'.hg.':
@@ -108,36 +106,43 b' class pathauditor:'
108 106 % (path, pycompat.bytestr(base))
109 107 )
110 108
111 normparts = util.splitpath(normpath)
112 assert len(parts) == len(normparts)
113
114 parts.pop()
115 normparts.pop()
116 # It's important that we check the path parts starting from the root.
117 # We don't want to add "foo/bar/baz" to auditeddir before checking if
118 # there's a "foo/.hg" directory. This also means we won't accidentally
119 # traverse a symlink into some other filesystem (which is potentially
120 # expensive to access).
121 for i in range(len(parts)):
122 prefix = pycompat.ossep.join(parts[: i + 1])
123 normprefix = pycompat.ossep.join(normparts[: i + 1])
124 if normprefix in self.auditeddir:
125 continue
126 if self._realfs:
127 self._checkfs(prefix, path)
128 if self._cached:
129 self.auditeddir.add(normprefix)
109 if self._realfs:
110 # It's important that we check the path parts starting from the root.
111 # We don't want to add "foo/bar/baz" to auditeddir before checking if
112 # there's a "foo/.hg" directory. This also means we won't accidentally
113 # traverse a symlink into some other filesystem (which is potentially
114 # expensive to access).
115 for prefix in finddirs_rev_noroot(path):
116 if prefix in self.auditeddir:
117 res = self.auditeddir[prefix]
118 else:
119 res = pathauditor._checkfs_exists(
120 self.root, prefix, path, self.callback
121 )
122 if self._cached:
123 self.auditeddir[prefix] = res
124 if not res:
125 break
130 126
131 127 if self._cached:
132 self.audited.add(normpath)
128 self.audited.add(path)
133 129
134 def _checkfs(self, prefix, path):
135 # type: (bytes, bytes) -> None
136 """raise exception if a file system backed check fails"""
137 curpath = os.path.join(self.root, prefix)
130 @staticmethod
131 def _checkfs_exists(
132 root,
133 prefix: bytes,
134 path: bytes,
135 callback: Optional[Callable[[bytes], bool]] = None,
136 ):
137 """raise exception if a file system backed check fails.
138
139 Return a bool that indicates that the directory (or file) exists."""
140 curpath = os.path.join(root, prefix)
138 141 try:
139 142 st = os.lstat(curpath)
140 143 except OSError as err:
144 if err.errno == errno.ENOENT:
145 return False
141 146 # EINVAL can be raised as invalid path syntax under win32.
142 147 # They must be ignored for patterns can be checked too.
143 148 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
@@ -152,9 +157,10 b' class pathauditor:'
152 157 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
153 158 os.path.join(curpath, b'.hg')
154 159 ):
155 if not self.callback or not self.callback(curpath):
160 if not callback or not callback(curpath):
156 161 msg = _(b"path '%s' is inside nested repo %r")
157 162 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
163 return True
158 164
159 165 def check(self, path):
160 166 # type: (bytes) -> bool
@@ -314,6 +320,13 b' def finddirs(path):'
314 320 yield b''
315 321
316 322
323 def finddirs_rev_noroot(path: bytes) -> Iterator[bytes]:
324 pos = path.find(pycompat.ossep)
325 while pos != -1:
326 yield path[:pos]
327 pos = path.find(pycompat.ossep, pos + 1)
328
329
317 330 class dirs:
318 331 '''a multiset of directory names from a set of file paths'''
319 332
@@ -76,7 +76,7 b' def _importfrom(pkgname, modname):'
76 76 ('cext', 'bdiff'): 3,
77 77 ('cext', 'mpatch'): 1,
78 78 ('cext', 'osutil'): 4,
79 ('cext', 'parsers'): 20,
79 ('cext', 'parsers'): 21,
80 80 }
81 81
82 82 # map import request to other package or module
@@ -17,8 +17,23 b' import select'
17 17 import stat
18 18 import sys
19 19 import tempfile
20 import typing
20 21 import unicodedata
21 22
23 from typing import (
24 Any,
25 AnyStr,
26 Iterable,
27 Iterator,
28 List,
29 Match,
30 NoReturn,
31 Optional,
32 Sequence,
33 Tuple,
34 Union,
35 )
36
22 37 from .i18n import _
23 38 from .pycompat import (
24 39 getattr,
@@ -44,7 +59,7 b' except AttributeError:'
44 59 # vaguely unix-like but don't have hardlink support. For those
45 60 # poor souls, just say we tried and that it failed so we fall back
46 61 # to copies.
47 def oslink(src, dst):
62 def oslink(src: bytes, dst: bytes) -> NoReturn:
48 63 raise OSError(
49 64 errno.EINVAL, b'hardlinks not supported: %s to %s' % (src, dst)
50 65 )
@@ -54,15 +69,47 b' readlink = os.readlink'
54 69 unlink = os.unlink
55 70 rename = os.rename
56 71 removedirs = os.removedirs
57 expandglobs = False
72
73 if typing.TYPE_CHECKING:
74 # Replace the various overloads that come along with aliasing stdlib methods
75 # with the narrow definition that we care about in the type checking phase
76 # only. This ensures that both Windows and POSIX see only the definition
77 # that is actually available.
78 #
79 # Note that if we check pycompat.TYPE_CHECKING here, it is always False, and
80 # the methods aren't replaced.
81
82 def normpath(path: bytes) -> bytes:
83 raise NotImplementedError
84
85 def abspath(path: AnyStr) -> AnyStr:
86 raise NotImplementedError
58 87
59 umask = os.umask(0)
88 def oslink(src: bytes, dst: bytes) -> None:
89 raise NotImplementedError
90
91 def readlink(path: bytes) -> bytes:
92 raise NotImplementedError
93
94 def unlink(path: bytes) -> None:
95 raise NotImplementedError
96
97 def rename(src: bytes, dst: bytes) -> None:
98 raise NotImplementedError
99
100 def removedirs(name: bytes) -> None:
101 raise NotImplementedError
102
103
104 expandglobs: bool = False
105
106 umask: int = os.umask(0)
60 107 os.umask(umask)
61 108
62 109 posixfile = open
63 110
64 111
65 def split(p):
112 def split(p: bytes) -> Tuple[bytes, bytes]:
66 113 """Same as posixpath.split, but faster
67 114
68 115 >>> import posixpath
@@ -85,17 +132,17 b' def split(p):'
85 132 return ht[0] + b'/', ht[1]
86 133
87 134
88 def openhardlinks():
135 def openhardlinks() -> bool:
89 136 '''return true if it is safe to hold open file handles to hardlinks'''
90 137 return True
91 138
92 139
93 def nlinks(name):
140 def nlinks(name: bytes) -> int:
94 141 '''return number of hardlinks for the given file'''
95 142 return os.lstat(name).st_nlink
96 143
97 144
98 def parsepatchoutput(output_line):
145 def parsepatchoutput(output_line: bytes) -> bytes:
99 146 """parses the output produced by patch and returns the filename"""
100 147 pf = output_line[14:]
101 148 if pycompat.sysplatform == b'OpenVMS':
@@ -107,7 +154,9 b' def parsepatchoutput(output_line):'
107 154 return pf
108 155
109 156
110 def sshargs(sshcmd, host, user, port):
157 def sshargs(
158 sshcmd: bytes, host: bytes, user: Optional[bytes], port: Optional[bytes]
159 ) -> bytes:
111 160 '''Build argument list for ssh'''
112 161 args = user and (b"%s@%s" % (user, host)) or host
113 162 if b'-' in args[:1]:
@@ -120,12 +169,12 b' def sshargs(sshcmd, host, user, port):'
120 169 return args
121 170
122 171
123 def isexec(f):
172 def isexec(f: bytes) -> bool:
124 173 """check whether a file is executable"""
125 174 return os.lstat(f).st_mode & 0o100 != 0
126 175
127 176
128 def setflags(f, l, x):
177 def setflags(f: bytes, l: bool, x: bool) -> None:
129 178 st = os.lstat(f)
130 179 s = st.st_mode
131 180 if l:
@@ -169,7 +218,12 b' def setflags(f, l, x):'
169 218 os.chmod(f, s & 0o666)
170 219
171 220
172 def copymode(src, dst, mode=None, enforcewritable=False):
221 def copymode(
222 src: bytes,
223 dst: bytes,
224 mode: Optional[bytes] = None,
225 enforcewritable: bool = False,
226 ) -> None:
173 227 """Copy the file mode from the file at path src to dst.
174 228 If src doesn't exist, we're using mode instead. If mode is None, we're
175 229 using umask."""
@@ -189,7 +243,7 b' def copymode(src, dst, mode=None, enforc'
189 243 os.chmod(dst, new_mode)
190 244
191 245
192 def checkexec(path):
246 def checkexec(path: bytes) -> bool:
193 247 """
194 248 Check whether the given path is on a filesystem with UNIX-like exec flags
195 249
@@ -230,7 +284,7 b' def checkexec(path):'
230 284 else:
231 285 # checkisexec exists, check if it actually is exec
232 286 if m & EXECFLAGS != 0:
233 # ensure checkisexec exists, check it isn't exec
287 # ensure checknoexec exists, check it isn't exec
234 288 try:
235 289 m = os.stat(checknoexec).st_mode
236 290 except FileNotFoundError:
@@ -269,7 +323,7 b' def checkexec(path):'
269 323 return False
270 324
271 325
272 def checklink(path):
326 def checklink(path: bytes) -> bool:
273 327 """check whether the given path is on a symlink-capable filesystem"""
274 328 # mktemp is not racy because symlink creation will fail if the
275 329 # file already exists
@@ -334,13 +388,13 b' def checklink(path):'
334 388 return False
335 389
336 390
337 def checkosfilename(path):
391 def checkosfilename(path: bytes) -> Optional[bytes]:
338 392 """Check that the base-relative path is a valid filename on this platform.
339 393 Returns None if the path is ok, or a UI string describing the problem."""
340 394 return None # on posix platforms, every path is ok
341 395
342 396
343 def getfsmountpoint(dirpath):
397 def getfsmountpoint(dirpath: bytes) -> Optional[bytes]:
344 398 """Get the filesystem mount point from a directory (best-effort)
345 399
346 400 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
@@ -348,7 +402,7 b' def getfsmountpoint(dirpath):'
348 402 return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath)
349 403
350 404
351 def getfstype(dirpath):
405 def getfstype(dirpath: bytes) -> Optional[bytes]:
352 406 """Get the filesystem type name from a directory (best-effort)
353 407
354 408 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
@@ -356,29 +410,29 b' def getfstype(dirpath):'
356 410 return getattr(osutil, 'getfstype', lambda x: None)(dirpath)
357 411
358 412
359 def get_password():
413 def get_password() -> bytes:
360 414 return encoding.strtolocal(getpass.getpass(''))
361 415
362 416
363 def setbinary(fd):
417 def setbinary(fd) -> None:
364 418 pass
365 419
366 420
367 def pconvert(path):
421 def pconvert(path: bytes) -> bytes:
368 422 return path
369 423
370 424
371 def localpath(path):
425 def localpath(path: bytes) -> bytes:
372 426 return path
373 427
374 428
375 def samefile(fpath1, fpath2):
429 def samefile(fpath1: bytes, fpath2: bytes) -> bool:
376 430 """Returns whether path1 and path2 refer to the same file. This is only
377 431 guaranteed to work for files, not directories."""
378 432 return os.path.samefile(fpath1, fpath2)
379 433
380 434
381 def samedevice(fpath1, fpath2):
435 def samedevice(fpath1: bytes, fpath2: bytes) -> bool:
382 436 """Returns whether fpath1 and fpath2 are on the same device. This is only
383 437 guaranteed to work for files, not directories."""
384 438 st1 = os.lstat(fpath1)
@@ -387,18 +441,18 b' def samedevice(fpath1, fpath2):'
387 441
388 442
389 443 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
390 def normcase(path):
444 def normcase(path: bytes) -> bytes:
391 445 return path.lower()
392 446
393 447
394 448 # what normcase does to ASCII strings
395 normcasespec = encoding.normcasespecs.lower
449 normcasespec: int = encoding.normcasespecs.lower
396 450 # fallback normcase function for non-ASCII strings
397 451 normcasefallback = normcase
398 452
399 453 if pycompat.isdarwin:
400 454
401 def normcase(path):
455 def normcase(path: bytes) -> bytes:
402 456 """
403 457 Normalize a filename for OS X-compatible comparison:
404 458 - escape-encode invalid characters
@@ -423,7 +477,7 b' if pycompat.isdarwin:'
423 477
424 478 normcasespec = encoding.normcasespecs.lower
425 479
426 def normcasefallback(path):
480 def normcasefallback(path: bytes) -> bytes:
427 481 try:
428 482 u = path.decode('utf-8')
429 483 except UnicodeDecodeError:
@@ -464,7 +518,7 b" if pycompat.sysplatform == b'cygwin':"
464 518 )
465 519
466 520 # use upper-ing as normcase as same as NTFS workaround
467 def normcase(path):
521 def normcase(path: bytes) -> bytes:
468 522 pathlen = len(path)
469 523 if (pathlen == 0) or (path[0] != pycompat.ossep):
470 524 # treat as relative
@@ -490,20 +544,20 b" if pycompat.sysplatform == b'cygwin':"
490 544 # but these translations are not supported by native
491 545 # tools, so the exec bit tends to be set erroneously.
492 546 # Therefore, disable executable bit access on Cygwin.
493 def checkexec(path):
547 def checkexec(path: bytes) -> bool:
494 548 return False
495 549
496 550 # Similarly, Cygwin's symlink emulation is likely to create
497 551 # problems when Mercurial is used from both Cygwin and native
498 552 # Windows, with other native tools, or on shared volumes
499 def checklink(path):
553 def checklink(path: bytes) -> bool:
500 554 return False
501 555
502 556
503 _needsshellquote = None
557 _needsshellquote: Optional[Match[bytes]] = None
504 558
505 559
506 def shellquote(s):
560 def shellquote(s: bytes) -> bytes:
507 561 if pycompat.sysplatform == b'OpenVMS':
508 562 return b'"%s"' % s
509 563 global _needsshellquote
@@ -516,12 +570,12 b' def shellquote(s):'
516 570 return b"'%s'" % s.replace(b"'", b"'\\''")
517 571
518 572
519 def shellsplit(s):
573 def shellsplit(s: bytes) -> List[bytes]:
520 574 """Parse a command string in POSIX shell way (best-effort)"""
521 575 return pycompat.shlexsplit(s, posix=True)
522 576
523 577
524 def testpid(pid):
578 def testpid(pid: int) -> bool:
525 579 '''return False if pid dead, True if running or not sure'''
526 580 if pycompat.sysplatform == b'OpenVMS':
527 581 return True
@@ -532,12 +586,12 b' def testpid(pid):'
532 586 return inst.errno != errno.ESRCH
533 587
534 588
535 def isowner(st):
589 def isowner(st: os.stat_result) -> bool:
536 590 """Return True if the stat object st is from the current user."""
537 591 return st.st_uid == os.getuid()
538 592
539 593
540 def findexe(command):
594 def findexe(command: bytes) -> Optional[bytes]:
541 595 """Find executable for command searching like which does.
542 596 If command is a basename then PATH is searched for command.
543 597 PATH isn't searched if command is an absolute or relative path.
@@ -545,7 +599,7 b' def findexe(command):'
545 599 if pycompat.sysplatform == b'OpenVMS':
546 600 return command
547 601
548 def findexisting(executable):
602 def findexisting(executable: bytes) -> Optional[bytes]:
549 603 b'Will return executable if existing file'
550 604 if os.path.isfile(executable) and os.access(executable, os.X_OK):
551 605 return executable
@@ -564,14 +618,14 b' def findexe(command):'
564 618 return None
565 619
566 620
567 def setsignalhandler():
621 def setsignalhandler() -> None:
568 622 pass
569 623
570 624
571 625 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
572 626
573 627
574 def statfiles(files):
628 def statfiles(files: Sequence[bytes]) -> Iterator[Optional[os.stat_result]]:
575 629 """Stat each file in files. Yield each stat, or None if a file does not
576 630 exist or has a type we don't care about."""
577 631 lstat = os.lstat
@@ -586,12 +640,12 b' def statfiles(files):'
586 640 yield st
587 641
588 642
589 def getuser():
643 def getuser() -> bytes:
590 644 '''return name of current user'''
591 645 return pycompat.fsencode(getpass.getuser())
592 646
593 647
594 def username(uid=None):
648 def username(uid: Optional[int] = None) -> Optional[bytes]:
595 649 """Return the name of the user with the given uid.
596 650
597 651 If uid is None, return the name of the current user."""
@@ -604,7 +658,7 b' def username(uid=None):'
604 658 return b'%d' % uid
605 659
606 660
607 def groupname(gid=None):
661 def groupname(gid: Optional[int] = None) -> Optional[bytes]:
608 662 """Return the name of the group with the given gid.
609 663
610 664 If gid is None, return the name of the current group."""
@@ -617,7 +671,7 b' def groupname(gid=None):'
617 671 return pycompat.bytestr(gid)
618 672
619 673
620 def groupmembers(name):
674 def groupmembers(name: bytes) -> List[bytes]:
621 675 """Return the list of members of the group with the given
622 676 name, KeyError if the group does not exist.
623 677 """
@@ -625,23 +679,27 b' def groupmembers(name):'
625 679 return pycompat.rapply(pycompat.fsencode, list(grp.getgrnam(name).gr_mem))
626 680
627 681
628 def spawndetached(args):
682 def spawndetached(args: List[bytes]) -> int:
629 683 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0), args[0], args)
630 684
631 685
632 def gethgcmd():
686 def gethgcmd(): # TODO: convert to bytes, like on Windows?
633 687 return sys.argv[:1]
634 688
635 689
636 def makedir(path, notindexed):
690 def makedir(path: bytes, notindexed: bool) -> None:
637 691 os.mkdir(path)
638 692
639 693
640 def lookupreg(key, name=None, scope=None):
694 def lookupreg(
695 key: bytes,
696 name: Optional[bytes] = None,
697 scope: Optional[Union[int, Iterable[int]]] = None,
698 ) -> Optional[bytes]:
641 699 return None
642 700
643 701
644 def hidewindow():
702 def hidewindow() -> None:
645 703 """Hide current shell window.
646 704
647 705 Used to hide the window opened when starting asynchronous
@@ -651,15 +709,15 b' def hidewindow():'
651 709
652 710
653 711 class cachestat:
654 def __init__(self, path):
712 def __init__(self, path: bytes) -> None:
655 713 self.stat = os.stat(path)
656 714
657 def cacheable(self):
715 def cacheable(self) -> bool:
658 716 return bool(self.stat.st_ino)
659 717
660 718 __hash__ = object.__hash__
661 719
662 def __eq__(self, other):
720 def __eq__(self, other: Any) -> bool:
663 721 try:
664 722 # Only dev, ino, size, mtime and atime are likely to change. Out
665 723 # of these, we shouldn't compare atime but should compare the
@@ -680,18 +738,18 b' class cachestat:'
680 738 except AttributeError:
681 739 return False
682 740
683 def __ne__(self, other):
741 def __ne__(self, other: Any) -> bool:
684 742 return not self == other
685 743
686 744
687 def statislink(st):
745 def statislink(st: Optional[os.stat_result]) -> bool:
688 746 '''check whether a stat result is a symlink'''
689 return st and stat.S_ISLNK(st.st_mode)
747 return stat.S_ISLNK(st.st_mode) if st else False
690 748
691 749
692 def statisexec(st):
750 def statisexec(st: Optional[os.stat_result]) -> bool:
693 751 '''check whether a stat result is an executable file'''
694 return st and (st.st_mode & 0o100 != 0)
752 return (st.st_mode & 0o100 != 0) if st else False
695 753
696 754
697 755 def poll(fds):
@@ -708,7 +766,7 b' def poll(fds):'
708 766 return sorted(list(set(sum(res, []))))
709 767
710 768
711 def readpipe(pipe):
769 def readpipe(pipe) -> bytes:
712 770 """Read all available data from a pipe."""
713 771 # We can't fstat() a pipe because Linux will always report 0.
714 772 # So, we set the pipe to non-blocking mode and read everything
@@ -733,7 +791,7 b' def readpipe(pipe):'
733 791 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
734 792
735 793
736 def bindunixsocket(sock, path):
794 def bindunixsocket(sock, path: bytes) -> None:
737 795 """Bind the UNIX domain socket to the specified path"""
738 796 # use relative path instead of full path at bind() if possible, since
739 797 # AF_UNIX path has very small length limit (107 chars) on common
@@ -10,8 +10,13 b' import difflib'
10 10 import re
11 11 import struct
12 12
13 from typing import (
14 List,
15 Tuple,
16 )
13 17
14 def splitnewlines(text):
18
19 def splitnewlines(text: bytes) -> List[bytes]:
15 20 '''like str.splitlines, but only split on newlines.'''
16 21 lines = [l + b'\n' for l in text.split(b'\n')]
17 22 if lines:
@@ -22,7 +27,9 b' def splitnewlines(text):'
22 27 return lines
23 28
24 29
25 def _normalizeblocks(a, b, blocks):
30 def _normalizeblocks(
31 a: List[bytes], b: List[bytes], blocks
32 ) -> List[Tuple[int, int, int]]:
26 33 prev = None
27 34 r = []
28 35 for curr in blocks:
@@ -57,7 +64,7 b' def _normalizeblocks(a, b, blocks):'
57 64 return r
58 65
59 66
60 def bdiff(a, b):
67 def bdiff(a: bytes, b: bytes) -> bytes:
61 68 a = bytes(a).splitlines(True)
62 69 b = bytes(b).splitlines(True)
63 70
@@ -84,7 +91,7 b' def bdiff(a, b):'
84 91 return b"".join(bin)
85 92
86 93
87 def blocks(a, b):
94 def blocks(a: bytes, b: bytes) -> List[Tuple[int, int, int, int]]:
88 95 an = splitnewlines(a)
89 96 bn = splitnewlines(b)
90 97 d = difflib.SequenceMatcher(None, an, bn).get_matching_blocks()
@@ -92,7 +99,7 b' def blocks(a, b):'
92 99 return [(i, i + n, j, j + n) for (i, j, n) in d]
93 100
94 101
95 def fixws(text, allws):
102 def fixws(text: bytes, allws: bool) -> bytes:
96 103 if allws:
97 104 text = re.sub(b'[ \t\r]+', b'', text)
98 105 else:
@@ -9,6 +9,11 b''
9 9 import io
10 10 import struct
11 11
12 from typing import (
13 List,
14 Tuple,
15 )
16
12 17
13 18 stringio = io.BytesIO
14 19
@@ -28,7 +33,9 b' class mpatchError(Exception):'
28 33 # temporary string buffers.
29 34
30 35
31 def _pull(dst, src, l): # pull l bytes from src
36 def _pull(
37 dst: List[Tuple[int, int]], src: List[Tuple[int, int]], l: int
38 ) -> None: # pull l bytes from src
32 39 while l:
33 40 f = src.pop()
34 41 if f[0] > l: # do we need to split?
@@ -39,7 +46,7 b' def _pull(dst, src, l): # pull l bytes '
39 46 l -= f[0]
40 47
41 48
42 def _move(m, dest, src, count):
49 def _move(m: stringio, dest: int, src: int, count: int) -> None:
43 50 """move count bytes from src to dest
44 51
45 52 The file pointer is left at the end of dest.
@@ -50,7 +57,9 b' def _move(m, dest, src, count):'
50 57 m.write(buf)
51 58
52 59
53 def _collect(m, buf, list):
60 def _collect(
61 m: stringio, buf: int, list: List[Tuple[int, int]]
62 ) -> Tuple[int, int]:
54 63 start = buf
55 64 for l, p in reversed(list):
56 65 _move(m, buf, p, l)
@@ -58,7 +67,7 b' def _collect(m, buf, list):'
58 67 return (buf - start, start)
59 68
60 69
61 def patches(a, bins):
70 def patches(a: bytes, bins: List[bytes]) -> bytes:
62 71 if not bins:
63 72 return a
64 73
@@ -111,7 +120,7 b' def patches(a, bins):'
111 120 return m.read(t[0])
112 121
113 122
114 def patchedsize(orig, delta):
123 def patchedsize(orig: int, delta: bytes) -> int:
115 124 outlen, last, bin = 0, 0, 0
116 125 binend = len(delta)
117 126 data = 12
@@ -435,6 +435,11 b' class DirstateItem:'
435 435 return self._wc_tracked and not (self._p1_tracked or self._p2_info)
436 436
437 437 @property
438 def modified(self):
439 """True if the file has been modified"""
440 return self._wc_tracked and self._p1_tracked and self._p2_info
441
442 @property
438 443 def maybe_clean(self):
439 444 """True if the file has a chance to be in the "clean" state"""
440 445 if not self._wc_tracked:
@@ -28,6 +28,24 b' import sys'
28 28 import tempfile
29 29 import xmlrpc.client as xmlrpclib
30 30
31 from typing import (
32 Any,
33 AnyStr,
34 BinaryIO,
35 Dict,
36 Iterable,
37 Iterator,
38 List,
39 Mapping,
40 NoReturn,
41 Optional,
42 Sequence,
43 Tuple,
44 Type,
45 TypeVar,
46 cast,
47 overload,
48 )
31 49
32 50 ispy3 = sys.version_info[0] >= 3
33 51 ispypy = '__pypy__' in sys.builtin_module_names
@@ -38,6 +56,10 b' if not globals(): # hide this from non-'
38 56
39 57 TYPE_CHECKING = typing.TYPE_CHECKING
40 58
59 _GetOptResult = Tuple[List[Tuple[bytes, bytes]], List[bytes]]
60 _T0 = TypeVar('_T0')
61 _Tbytestr = TypeVar('_Tbytestr', bound='bytestr')
62
41 63
42 64 def future_set_exception_info(f, exc_info):
43 65 f.set_exception(exc_info[0])
@@ -46,7 +68,7 b' def future_set_exception_info(f, exc_inf'
46 68 FileNotFoundError = builtins.FileNotFoundError
47 69
48 70
49 def identity(a):
71 def identity(a: _T0) -> _T0:
50 72 return a
51 73
52 74
@@ -94,21 +116,17 b" if os.name == r'nt':"
94 116
95 117 fsencode = os.fsencode
96 118 fsdecode = os.fsdecode
97 oscurdir = os.curdir.encode('ascii')
98 oslinesep = os.linesep.encode('ascii')
99 osname = os.name.encode('ascii')
100 ospathsep = os.pathsep.encode('ascii')
101 ospardir = os.pardir.encode('ascii')
102 ossep = os.sep.encode('ascii')
103 osaltsep = os.altsep
104 if osaltsep:
105 osaltsep = osaltsep.encode('ascii')
106 osdevnull = os.devnull.encode('ascii')
119 oscurdir: bytes = os.curdir.encode('ascii')
120 oslinesep: bytes = os.linesep.encode('ascii')
121 osname: bytes = os.name.encode('ascii')
122 ospathsep: bytes = os.pathsep.encode('ascii')
123 ospardir: bytes = os.pardir.encode('ascii')
124 ossep: bytes = os.sep.encode('ascii')
125 osaltsep: Optional[bytes] = os.altsep.encode('ascii') if os.altsep else None
126 osdevnull: bytes = os.devnull.encode('ascii')
107 127
108 sysplatform = sys.platform.encode('ascii')
109 sysexecutable = sys.executable
110 if sysexecutable:
111 sysexecutable = os.fsencode(sysexecutable)
128 sysplatform: bytes = sys.platform.encode('ascii')
129 sysexecutable: bytes = os.fsencode(sys.executable) if sys.executable else b''
112 130
113 131
114 132 def maplist(*args):
@@ -128,7 +146,7 b' getargspec = inspect.getfullargspec'
128 146
129 147 long = int
130 148
131 if getattr(sys, 'argv', None) is not None:
149 if builtins.getattr(sys, 'argv', None) is not None:
132 150 # On POSIX, the char** argv array is converted to Python str using
133 151 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
134 152 # isn't directly callable from Python code. In practice, os.fsencode()
@@ -143,6 +161,7 b" if getattr(sys, 'argv', None) is not Non"
143 161 # (this is how Python 2 worked). To get that, we encode with the mbcs
144 162 # encoding, which will pass CP_ACP to the underlying Windows API to
145 163 # produce bytes.
164 sysargv: List[bytes] = []
146 165 if os.name == r'nt':
147 166 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
148 167 else:
@@ -211,38 +230,53 b' class bytestr(bytes):'
211 230 # https://github.com/google/pytype/issues/500
212 231 if TYPE_CHECKING:
213 232
214 def __init__(self, s=b''):
233 def __init__(self, s: object = b'') -> None:
215 234 pass
216 235
217 def __new__(cls, s=b''):
236 def __new__(cls: Type[_Tbytestr], s: object = b'') -> _Tbytestr:
218 237 if isinstance(s, bytestr):
219 238 return s
220 239 if not isinstance(
221 240 s, (bytes, bytearray)
222 ) and not hasattr( # hasattr-py3-only
241 ) and not builtins.hasattr( # hasattr-py3-only
223 242 s, u'__bytes__'
224 243 ):
225 244 s = str(s).encode('ascii')
226 245 return bytes.__new__(cls, s)
227 246
228 def __getitem__(self, key):
247 # The base class uses `int` return in py3, but the point of this class is to
248 # behave like py2.
249 def __getitem__(self, key) -> bytes: # pytype: disable=signature-mismatch
229 250 s = bytes.__getitem__(self, key)
230 251 if not isinstance(s, bytes):
231 252 s = bytechr(s)
232 253 return s
233 254
234 def __iter__(self):
255 # The base class expects `Iterator[int]` return in py3, but the point of
256 # this class is to behave like py2.
257 def __iter__(self) -> Iterator[bytes]: # pytype: disable=signature-mismatch
235 258 return iterbytestr(bytes.__iter__(self))
236 259
237 def __repr__(self):
260 def __repr__(self) -> str:
238 261 return bytes.__repr__(self)[1:] # drop b''
239 262
240 263
241 def iterbytestr(s):
264 def iterbytestr(s: Iterable[int]) -> Iterator[bytes]:
242 265 """Iterate bytes as if it were a str object of Python 2"""
243 266 return map(bytechr, s)
244 267
245 268
269 if TYPE_CHECKING:
270
271 @overload
272 def maybebytestr(s: bytes) -> bytestr:
273 ...
274
275 @overload
276 def maybebytestr(s: _T0) -> _T0:
277 ...
278
279
246 280 def maybebytestr(s):
247 281 """Promote bytes to bytestr"""
248 282 if isinstance(s, bytes):
@@ -250,7 +284,7 b' def maybebytestr(s):'
250 284 return s
251 285
252 286
253 def sysbytes(s):
287 def sysbytes(s: AnyStr) -> bytes:
254 288 """Convert an internal str (e.g. keyword, __doc__) back to bytes
255 289
256 290 This never raises UnicodeEncodeError, but only ASCII characters
@@ -261,7 +295,7 b' def sysbytes(s):'
261 295 return s.encode('utf-8')
262 296
263 297
264 def sysstr(s):
298 def sysstr(s: AnyStr) -> str:
265 299 """Return a keyword str to be passed to Python functions such as
266 300 getattr() and str.encode()
267 301
@@ -274,29 +308,29 b' def sysstr(s):'
274 308 return s.decode('latin-1')
275 309
276 310
277 def strurl(url):
311 def strurl(url: AnyStr) -> str:
278 312 """Converts a bytes url back to str"""
279 313 if isinstance(url, bytes):
280 314 return url.decode('ascii')
281 315 return url
282 316
283 317
284 def bytesurl(url):
318 def bytesurl(url: AnyStr) -> bytes:
285 319 """Converts a str url to bytes by encoding in ascii"""
286 320 if isinstance(url, str):
287 321 return url.encode('ascii')
288 322 return url
289 323
290 324
291 def raisewithtb(exc, tb):
325 def raisewithtb(exc: BaseException, tb) -> NoReturn:
292 326 """Raise exception with the given traceback"""
293 327 raise exc.with_traceback(tb)
294 328
295 329
296 def getdoc(obj):
330 def getdoc(obj: object) -> Optional[bytes]:
297 331 """Get docstring as bytes; may be None so gettext() won't confuse it
298 332 with _('')"""
299 doc = getattr(obj, '__doc__', None)
333 doc = builtins.getattr(obj, '__doc__', None)
300 334 if doc is None:
301 335 return doc
302 336 return sysbytes(doc)
@@ -319,14 +353,22 b' xrange = builtins.range'
319 353 unicode = str
320 354
321 355
322 def open(name, mode=b'r', buffering=-1, encoding=None):
356 def open(
357 name,
358 mode: AnyStr = b'r',
359 buffering: int = -1,
360 encoding: Optional[str] = None,
361 ) -> Any:
362 # TODO: assert binary mode, and cast result to BinaryIO?
323 363 return builtins.open(name, sysstr(mode), buffering, encoding)
324 364
325 365
326 366 safehasattr = _wrapattrfunc(builtins.hasattr)
327 367
328 368
329 def _getoptbwrapper(orig, args, shortlist, namelist):
369 def _getoptbwrapper(
370 orig, args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
371 ) -> _GetOptResult:
330 372 """
331 373 Takes bytes arguments, converts them to unicode, pass them to
332 374 getopt.getopt(), convert the returned values back to bytes and then
@@ -342,7 +384,7 b' def _getoptbwrapper(orig, args, shortlis'
342 384 return opts, args
343 385
344 386
345 def strkwargs(dic):
387 def strkwargs(dic: Mapping[bytes, _T0]) -> Dict[str, _T0]:
346 388 """
347 389 Converts the keys of a python dictonary to str i.e. unicodes so that
348 390 they can be passed as keyword arguments as dictionaries with bytes keys
@@ -352,7 +394,7 b' def strkwargs(dic):'
352 394 return dic
353 395
354 396
355 def byteskwargs(dic):
397 def byteskwargs(dic: Mapping[str, _T0]) -> Dict[bytes, _T0]:
356 398 """
357 399 Converts keys of python dictionaries to bytes as they were converted to
358 400 str to pass that dictonary as a keyword argument on Python 3.
@@ -362,7 +404,9 b' def byteskwargs(dic):'
362 404
363 405
364 406 # TODO: handle shlex.shlex().
365 def shlexsplit(s, comments=False, posix=True):
407 def shlexsplit(
408 s: bytes, comments: bool = False, posix: bool = True
409 ) -> List[bytes]:
366 410 """
367 411 Takes bytes argument, convert it to str i.e. unicodes, pass that into
368 412 shlex.split(), convert the returned value to bytes and return that for
@@ -377,46 +421,59 b' itervalues = lambda x: x.values()'
377 421
378 422 json_loads = json.loads
379 423
380 isjython = sysplatform.startswith(b'java')
424 isjython: bool = sysplatform.startswith(b'java')
381 425
382 isdarwin = sysplatform.startswith(b'darwin')
383 islinux = sysplatform.startswith(b'linux')
384 isposix = osname == b'posix'
385 iswindows = osname == b'nt'
426 isdarwin: bool = sysplatform.startswith(b'darwin')
427 islinux: bool = sysplatform.startswith(b'linux')
428 isposix: bool = osname == b'posix'
429 iswindows: bool = osname == b'nt'
386 430
387 431
388 def getoptb(args, shortlist, namelist):
432 def getoptb(
433 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
434 ) -> _GetOptResult:
389 435 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
390 436
391 437
392 def gnugetoptb(args, shortlist, namelist):
438 def gnugetoptb(
439 args: Sequence[bytes], shortlist: bytes, namelist: Sequence[bytes]
440 ) -> _GetOptResult:
393 441 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
394 442
395 443
396 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
444 def mkdtemp(
445 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
446 ) -> bytes:
397 447 return tempfile.mkdtemp(suffix, prefix, dir)
398 448
399 449
400 450 # text=True is not supported; use util.from/tonativeeol() instead
401 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
451 def mkstemp(
452 suffix: bytes = b'', prefix: bytes = b'tmp', dir: Optional[bytes] = None
453 ) -> Tuple[int, bytes]:
402 454 return tempfile.mkstemp(suffix, prefix, dir)
403 455
404 456
405 457 # TemporaryFile does not support an "encoding=" argument on python2.
406 458 # This wrapper file are always open in byte mode.
407 def unnamedtempfile(mode=None, *args, **kwargs):
459 def unnamedtempfile(mode: Optional[bytes] = None, *args, **kwargs) -> BinaryIO:
408 460 if mode is None:
409 461 mode = 'w+b'
410 462 else:
411 463 mode = sysstr(mode)
412 464 assert 'b' in mode
413 return tempfile.TemporaryFile(mode, *args, **kwargs)
465 return cast(BinaryIO, tempfile.TemporaryFile(mode, *args, **kwargs))
414 466
415 467
416 468 # NamedTemporaryFile does not support an "encoding=" argument on python2.
417 469 # This wrapper file are always open in byte mode.
418 470 def namedtempfile(
419 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
471 mode: bytes = b'w+b',
472 bufsize: int = -1,
473 suffix: bytes = b'',
474 prefix: bytes = b'tmp',
475 dir: Optional[bytes] = None,
476 delete: bool = True,
420 477 ):
421 478 mode = sysstr(mode)
422 479 assert 'b' in mode
@@ -38,12 +38,15 b' from .revlogutils.constants import ('
38 38 COMP_MODE_DEFAULT,
39 39 COMP_MODE_INLINE,
40 40 COMP_MODE_PLAIN,
41 DELTA_BASE_REUSE_NO,
42 DELTA_BASE_REUSE_TRY,
41 43 ENTRY_RANK,
42 44 FEATURES_BY_VERSION,
43 45 FLAG_GENERALDELTA,
44 46 FLAG_INLINE_DATA,
45 47 INDEX_HEADER,
46 48 KIND_CHANGELOG,
49 KIND_FILELOG,
47 50 RANK_UNKNOWN,
48 51 REVLOGV0,
49 52 REVLOGV1,
@@ -125,7 +128,7 b" rustrevlog = policy.importrust('revlog')"
125 128 # Aliased for performance.
126 129 _zlibdecompress = zlib.decompress
127 130
128 # max size of revlog with inline data
131 # max size of inline data embedded into a revlog
129 132 _maxinline = 131072
130 133
131 134 # Flag processors for REVIDX_ELLIPSIS.
@@ -347,6 +350,7 b' class revlog:'
347 350 self._chunkcachesize = 65536
348 351 self._maxchainlen = None
349 352 self._deltabothparents = True
353 self._candidate_group_chunk_size = 0
350 354 self._debug_delta = False
351 355 self.index = None
352 356 self._docket = None
@@ -363,6 +367,11 b' class revlog:'
363 367 self._srdensitythreshold = 0.50
364 368 self._srmingapsize = 262144
365 369
370 # other optionnals features
371
372 # might remove rank configuration once the computation has no impact
373 self._compute_rank = False
374
366 375 # Make copy of flag processors so each revlog instance can support
367 376 # custom flags.
368 377 self._flagprocessors = dict(flagutil.flagprocessors)
@@ -404,6 +413,7 b' class revlog:'
404 413
405 414 if b'changelogv2' in opts and self.revlog_kind == KIND_CHANGELOG:
406 415 new_header = CHANGELOGV2
416 self._compute_rank = opts.get(b'changelogv2.compute-rank', True)
407 417 elif b'revlogv2' in opts:
408 418 new_header = REVLOGV2
409 419 elif b'revlogv1' in opts:
@@ -421,6 +431,9 b' class revlog:'
421 431 self._maxchainlen = opts[b'maxchainlen']
422 432 if b'deltabothparents' in opts:
423 433 self._deltabothparents = opts[b'deltabothparents']
434 dps_cgds = opts.get(b'delta-parent-search.candidate-group-chunk-size')
435 if dps_cgds:
436 self._candidate_group_chunk_size = dps_cgds
424 437 self._lazydelta = bool(opts.get(b'lazydelta', True))
425 438 self._lazydeltabase = False
426 439 if self._lazydelta:
@@ -505,7 +518,6 b' class revlog:'
505 518 self._docket = docket
506 519 self._docket_file = entry_point
507 520 else:
508 entry_data = b''
509 521 self._initempty = True
510 522 entry_data = self._get_data(entry_point, mmapindexthreshold)
511 523 if len(entry_data) > 0:
@@ -653,9 +665,12 b' class revlog:'
653 665 @util.propertycache
654 666 def display_id(self):
655 667 """The public facing "ID" of the revlog that we use in message"""
656 # Maybe we should build a user facing representation of
657 # revlog.target instead of using `self.radix`
658 return self.radix
668 if self.revlog_kind == KIND_FILELOG:
669 # Reference the file without the "data/" prefix, so it is familiar
670 # to the user.
671 return self.target[1]
672 else:
673 return self.radix
659 674
660 675 def _get_decompressor(self, t):
661 676 try:
@@ -2445,6 +2460,16 b' class revlog:'
2445 2460 self, write_debug=write_debug
2446 2461 )
2447 2462
2463 if cachedelta is not None and len(cachedelta) == 2:
2464 # If the cached delta has no information about how it should be
2465 # reused, add the default reuse instruction according to the
2466 # revlog's configuration.
2467 if self._generaldelta and self._lazydeltabase:
2468 delta_base_reuse = DELTA_BASE_REUSE_TRY
2469 else:
2470 delta_base_reuse = DELTA_BASE_REUSE_NO
2471 cachedelta = (cachedelta[0], cachedelta[1], delta_base_reuse)
2472
2448 2473 revinfo = revlogutils.revisioninfo(
2449 2474 node,
2450 2475 p1,
@@ -2492,7 +2517,7 b' class revlog:'
2492 2517 sidedata_offset = 0
2493 2518
2494 2519 rank = RANK_UNKNOWN
2495 if self._format_version == CHANGELOGV2:
2520 if self._compute_rank:
2496 2521 if (p1r, p2r) == (nullrev, nullrev):
2497 2522 rank = 1
2498 2523 elif p1r != nullrev and p2r == nullrev:
@@ -2637,6 +2662,8 b' class revlog:'
2637 2662 alwayscache=False,
2638 2663 addrevisioncb=None,
2639 2664 duplicaterevisioncb=None,
2665 debug_info=None,
2666 delta_base_reuse_policy=None,
2640 2667 ):
2641 2668 """
2642 2669 add a delta group
@@ -2652,6 +2679,14 b' class revlog:'
2652 2679 if self._adding_group:
2653 2680 raise error.ProgrammingError(b'cannot nest addgroup() calls')
2654 2681
2682 # read the default delta-base reuse policy from revlog config if the
2683 # group did not specify one.
2684 if delta_base_reuse_policy is None:
2685 if self._generaldelta and self._lazydeltabase:
2686 delta_base_reuse_policy = DELTA_BASE_REUSE_TRY
2687 else:
2688 delta_base_reuse_policy = DELTA_BASE_REUSE_NO
2689
2655 2690 self._adding_group = True
2656 2691 empty = True
2657 2692 try:
@@ -2662,6 +2697,7 b' class revlog:'
2662 2697 deltacomputer = deltautil.deltacomputer(
2663 2698 self,
2664 2699 write_debug=write_debug,
2700 debug_info=debug_info,
2665 2701 )
2666 2702 # loop through our set of deltas
2667 2703 for data in deltas:
@@ -2731,7 +2767,7 b' class revlog:'
2731 2767 p1,
2732 2768 p2,
2733 2769 flags,
2734 (baserev, delta),
2770 (baserev, delta, delta_base_reuse_policy),
2735 2771 alwayscache=alwayscache,
2736 2772 deltacomputer=deltacomputer,
2737 2773 sidedata=sidedata,
@@ -2886,6 +2922,7 b' class revlog:'
2886 2922 assumehaveparentrevisions=False,
2887 2923 deltamode=repository.CG_DELTAMODE_STD,
2888 2924 sidedata_helpers=None,
2925 debug_info=None,
2889 2926 ):
2890 2927 if nodesorder not in (b'nodes', b'storage', b'linear', None):
2891 2928 raise error.ProgrammingError(
@@ -2915,6 +2952,7 b' class revlog:'
2915 2952 revisiondata=revisiondata,
2916 2953 assumehaveparentrevisions=assumehaveparentrevisions,
2917 2954 sidedata_helpers=sidedata_helpers,
2955 debug_info=debug_info,
2918 2956 )
2919 2957
2920 2958 DELTAREUSEALWAYS = b'always'
@@ -67,7 +67,7 b' class revisioninfo:'
67 67 node: expected hash of the revision
68 68 p1, p2: parent revs of the revision
69 69 btext: built text cache consisting of a one-element list
70 cachedelta: (baserev, uncompressed_delta) or None
70 cachedelta: (baserev, uncompressed_delta, usage_mode) or None
71 71 flags: flags associated to the revision storage
72 72
73 73 One of btext[0] or cachedelta must be set.
@@ -301,3 +301,18 b' FEATURES_BY_VERSION = {'
301 301
302 302
303 303 SPARSE_REVLOG_MAX_CHAIN_LENGTH = 1000
304
305 ### What should be done with a cached delta and its base ?
306
307 # Ignore the cache when considering candidates.
308 #
309 # The cached delta might be used, but the delta base will not be scheduled for
310 # usage earlier than in "normal" order.
311 DELTA_BASE_REUSE_NO = 0
312
313 # Prioritize trying the cached delta base
314 #
315 # The delta base will be tested for validy first. So that the cached deltas get
316 # used when possible.
317 DELTA_BASE_REUSE_TRY = 1
318 DELTA_BASE_REUSE_FORCE = 2
This diff has been collapsed as it changes many lines, (503 lines changed) Show them Hide them
@@ -6,12 +6,19 b''
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 import collections
10 import string
11
9 12 from .. import (
13 mdiff,
10 14 node as nodemod,
15 revlogutils,
16 util,
11 17 )
12 18
13 19 from . import (
14 20 constants,
21 deltas as deltautil,
15 22 )
16 23
17 24 INDEX_ENTRY_DEBUG_COLUMN = []
@@ -216,3 +223,499 b' def debug_index('
216 223 fm.plain(b'\n')
217 224
218 225 fm.end()
226
227
228 def dump(ui, revlog):
229 """perform the work for `hg debugrevlog --dump"""
230 # XXX seems redundant with debug index ?
231 r = revlog
232 numrevs = len(r)
233 ui.write(
234 (
235 b"# rev p1rev p2rev start end deltastart base p1 p2"
236 b" rawsize totalsize compression heads chainlen\n"
237 )
238 )
239 ts = 0
240 heads = set()
241
242 for rev in range(numrevs):
243 dbase = r.deltaparent(rev)
244 if dbase == -1:
245 dbase = rev
246 cbase = r.chainbase(rev)
247 clen = r.chainlen(rev)
248 p1, p2 = r.parentrevs(rev)
249 rs = r.rawsize(rev)
250 ts = ts + rs
251 heads -= set(r.parentrevs(rev))
252 heads.add(rev)
253 try:
254 compression = ts / r.end(rev)
255 except ZeroDivisionError:
256 compression = 0
257 ui.write(
258 b"%5d %5d %5d %5d %5d %10d %4d %4d %4d %7d %9d "
259 b"%11d %5d %8d\n"
260 % (
261 rev,
262 p1,
263 p2,
264 r.start(rev),
265 r.end(rev),
266 r.start(dbase),
267 r.start(cbase),
268 r.start(p1),
269 r.start(p2),
270 rs,
271 ts,
272 compression,
273 len(heads),
274 clen,
275 )
276 )
277
278
279 def debug_revlog(ui, revlog):
280 """code for `hg debugrevlog`"""
281 r = revlog
282 format = r._format_version
283 v = r._format_flags
284 flags = []
285 gdelta = False
286 if v & constants.FLAG_INLINE_DATA:
287 flags.append(b'inline')
288 if v & constants.FLAG_GENERALDELTA:
289 gdelta = True
290 flags.append(b'generaldelta')
291 if not flags:
292 flags = [b'(none)']
293
294 ### the total size of stored content if incompressed.
295 full_text_total_size = 0
296 ### tracks merge vs single parent
297 nummerges = 0
298
299 ### tracks ways the "delta" are build
300 # nodelta
301 numempty = 0
302 numemptytext = 0
303 numemptydelta = 0
304 # full file content
305 numfull = 0
306 # intermediate snapshot against a prior snapshot
307 numsemi = 0
308 # snapshot count per depth
309 numsnapdepth = collections.defaultdict(lambda: 0)
310 # number of snapshots with a non-ancestor delta
311 numsnapdepth_nad = collections.defaultdict(lambda: 0)
312 # delta against previous revision
313 numprev = 0
314 # delta against prev, where prev is a non-ancestor
315 numprev_nad = 0
316 # delta against first or second parent (not prev)
317 nump1 = 0
318 nump2 = 0
319 # delta against neither prev nor parents
320 numother = 0
321 # delta against other that is a non-ancestor
322 numother_nad = 0
323 # delta against prev that are also first or second parent
324 # (details of `numprev`)
325 nump1prev = 0
326 nump2prev = 0
327
328 # data about delta chain of each revs
329 chainlengths = []
330 chainbases = []
331 chainspans = []
332
333 # data about each revision
334 datasize = [None, 0, 0]
335 fullsize = [None, 0, 0]
336 semisize = [None, 0, 0]
337 # snapshot count per depth
338 snapsizedepth = collections.defaultdict(lambda: [None, 0, 0])
339 deltasize = [None, 0, 0]
340 chunktypecounts = {}
341 chunktypesizes = {}
342
343 def addsize(size, l):
344 if l[0] is None or size < l[0]:
345 l[0] = size
346 if size > l[1]:
347 l[1] = size
348 l[2] += size
349
350 numrevs = len(r)
351 for rev in range(numrevs):
352 p1, p2 = r.parentrevs(rev)
353 delta = r.deltaparent(rev)
354 if format > 0:
355 s = r.rawsize(rev)
356 full_text_total_size += s
357 addsize(s, datasize)
358 if p2 != nodemod.nullrev:
359 nummerges += 1
360 size = r.length(rev)
361 if delta == nodemod.nullrev:
362 chainlengths.append(0)
363 chainbases.append(r.start(rev))
364 chainspans.append(size)
365 if size == 0:
366 numempty += 1
367 numemptytext += 1
368 else:
369 numfull += 1
370 numsnapdepth[0] += 1
371 addsize(size, fullsize)
372 addsize(size, snapsizedepth[0])
373 else:
374 nad = (
375 delta != p1 and delta != p2 and not r.isancestorrev(delta, rev)
376 )
377 chainlengths.append(chainlengths[delta] + 1)
378 baseaddr = chainbases[delta]
379 revaddr = r.start(rev)
380 chainbases.append(baseaddr)
381 chainspans.append((revaddr - baseaddr) + size)
382 if size == 0:
383 numempty += 1
384 numemptydelta += 1
385 elif r.issnapshot(rev):
386 addsize(size, semisize)
387 numsemi += 1
388 depth = r.snapshotdepth(rev)
389 numsnapdepth[depth] += 1
390 if nad:
391 numsnapdepth_nad[depth] += 1
392 addsize(size, snapsizedepth[depth])
393 else:
394 addsize(size, deltasize)
395 if delta == rev - 1:
396 numprev += 1
397 if delta == p1:
398 nump1prev += 1
399 elif delta == p2:
400 nump2prev += 1
401 elif nad:
402 numprev_nad += 1
403 elif delta == p1:
404 nump1 += 1
405 elif delta == p2:
406 nump2 += 1
407 elif delta != nodemod.nullrev:
408 numother += 1
409 numother_nad += 1
410
411 # Obtain data on the raw chunks in the revlog.
412 if util.safehasattr(r, '_getsegmentforrevs'):
413 segment = r._getsegmentforrevs(rev, rev)[1]
414 else:
415 segment = r._revlog._getsegmentforrevs(rev, rev)[1]
416 if segment:
417 chunktype = bytes(segment[0:1])
418 else:
419 chunktype = b'empty'
420
421 if chunktype not in chunktypecounts:
422 chunktypecounts[chunktype] = 0
423 chunktypesizes[chunktype] = 0
424
425 chunktypecounts[chunktype] += 1
426 chunktypesizes[chunktype] += size
427
428 # Adjust size min value for empty cases
429 for size in (datasize, fullsize, semisize, deltasize):
430 if size[0] is None:
431 size[0] = 0
432
433 numdeltas = numrevs - numfull - numempty - numsemi
434 numoprev = numprev - nump1prev - nump2prev - numprev_nad
435 num_other_ancestors = numother - numother_nad
436 totalrawsize = datasize[2]
437 datasize[2] /= numrevs
438 fulltotal = fullsize[2]
439 if numfull == 0:
440 fullsize[2] = 0
441 else:
442 fullsize[2] /= numfull
443 semitotal = semisize[2]
444 snaptotal = {}
445 if numsemi > 0:
446 semisize[2] /= numsemi
447 for depth in snapsizedepth:
448 snaptotal[depth] = snapsizedepth[depth][2]
449 snapsizedepth[depth][2] /= numsnapdepth[depth]
450
451 deltatotal = deltasize[2]
452 if numdeltas > 0:
453 deltasize[2] /= numdeltas
454 totalsize = fulltotal + semitotal + deltatotal
455 avgchainlen = sum(chainlengths) / numrevs
456 maxchainlen = max(chainlengths)
457 maxchainspan = max(chainspans)
458 compratio = 1
459 if totalsize:
460 compratio = totalrawsize / totalsize
461
462 basedfmtstr = b'%%%dd\n'
463 basepcfmtstr = b'%%%dd %s(%%5.2f%%%%)\n'
464
465 def dfmtstr(max):
466 return basedfmtstr % len(str(max))
467
468 def pcfmtstr(max, padding=0):
469 return basepcfmtstr % (len(str(max)), b' ' * padding)
470
471 def pcfmt(value, total):
472 if total:
473 return (value, 100 * float(value) / total)
474 else:
475 return value, 100.0
476
477 ui.writenoi18n(b'format : %d\n' % format)
478 ui.writenoi18n(b'flags : %s\n' % b', '.join(flags))
479
480 ui.write(b'\n')
481 fmt = pcfmtstr(totalsize)
482 fmt2 = dfmtstr(totalsize)
483 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
484 ui.writenoi18n(b' merges : ' + fmt % pcfmt(nummerges, numrevs))
485 ui.writenoi18n(
486 b' normal : ' + fmt % pcfmt(numrevs - nummerges, numrevs)
487 )
488 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
489 ui.writenoi18n(b' empty : ' + fmt % pcfmt(numempty, numrevs))
490 ui.writenoi18n(
491 b' text : '
492 + fmt % pcfmt(numemptytext, numemptytext + numemptydelta)
493 )
494 ui.writenoi18n(
495 b' delta : '
496 + fmt % pcfmt(numemptydelta, numemptytext + numemptydelta)
497 )
498 ui.writenoi18n(
499 b' snapshot : ' + fmt % pcfmt(numfull + numsemi, numrevs)
500 )
501 for depth in sorted(numsnapdepth):
502 base = b' lvl-%-3d : ' % depth
503 count = fmt % pcfmt(numsnapdepth[depth], numrevs)
504 pieces = [base, count]
505 if numsnapdepth_nad[depth]:
506 pieces[-1] = count = count[:-1] # drop the final '\n'
507 more = b' non-ancestor-bases: '
508 anc_count = fmt
509 anc_count %= pcfmt(numsnapdepth_nad[depth], numsnapdepth[depth])
510 pieces.append(more)
511 pieces.append(anc_count)
512 ui.write(b''.join(pieces))
513 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(numdeltas, numrevs))
514 ui.writenoi18n(b'revision size : ' + fmt2 % totalsize)
515 ui.writenoi18n(
516 b' snapshot : ' + fmt % pcfmt(fulltotal + semitotal, totalsize)
517 )
518 for depth in sorted(numsnapdepth):
519 ui.write(
520 (b' lvl-%-3d : ' % depth)
521 + fmt % pcfmt(snaptotal[depth], totalsize)
522 )
523 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(deltatotal, totalsize))
524
525 letters = string.ascii_letters.encode('ascii')
526
527 def fmtchunktype(chunktype):
528 if chunktype == b'empty':
529 return b' %s : ' % chunktype
530 elif chunktype in letters:
531 return b' 0x%s (%s) : ' % (nodemod.hex(chunktype), chunktype)
532 else:
533 return b' 0x%s : ' % nodemod.hex(chunktype)
534
535 ui.write(b'\n')
536 ui.writenoi18n(b'chunks : ' + fmt2 % numrevs)
537 for chunktype in sorted(chunktypecounts):
538 ui.write(fmtchunktype(chunktype))
539 ui.write(fmt % pcfmt(chunktypecounts[chunktype], numrevs))
540 ui.writenoi18n(b'chunks size : ' + fmt2 % totalsize)
541 for chunktype in sorted(chunktypecounts):
542 ui.write(fmtchunktype(chunktype))
543 ui.write(fmt % pcfmt(chunktypesizes[chunktype], totalsize))
544
545 ui.write(b'\n')
546 b_total = b"%d" % full_text_total_size
547 p_total = []
548 while len(b_total) > 3:
549 p_total.append(b_total[-3:])
550 b_total = b_total[:-3]
551 p_total.append(b_total)
552 p_total.reverse()
553 b_total = b' '.join(p_total)
554
555 ui.write(b'\n')
556 ui.writenoi18n(b'total-stored-content: %s bytes\n' % b_total)
557 ui.write(b'\n')
558 fmt = dfmtstr(max(avgchainlen, maxchainlen, maxchainspan, compratio))
559 ui.writenoi18n(b'avg chain length : ' + fmt % avgchainlen)
560 ui.writenoi18n(b'max chain length : ' + fmt % maxchainlen)
561 ui.writenoi18n(b'max chain reach : ' + fmt % maxchainspan)
562 ui.writenoi18n(b'compression ratio : ' + fmt % compratio)
563
564 if format > 0:
565 ui.write(b'\n')
566 ui.writenoi18n(
567 b'uncompressed data size (min/max/avg) : %d / %d / %d\n'
568 % tuple(datasize)
569 )
570 ui.writenoi18n(
571 b'full revision size (min/max/avg) : %d / %d / %d\n'
572 % tuple(fullsize)
573 )
574 ui.writenoi18n(
575 b'inter-snapshot size (min/max/avg) : %d / %d / %d\n'
576 % tuple(semisize)
577 )
578 for depth in sorted(snapsizedepth):
579 if depth == 0:
580 continue
581 ui.writenoi18n(
582 b' level-%-3d (min/max/avg) : %d / %d / %d\n'
583 % ((depth,) + tuple(snapsizedepth[depth]))
584 )
585 ui.writenoi18n(
586 b'delta size (min/max/avg) : %d / %d / %d\n'
587 % tuple(deltasize)
588 )
589
590 if numdeltas > 0:
591 ui.write(b'\n')
592 fmt = pcfmtstr(numdeltas)
593 fmt2 = pcfmtstr(numdeltas, 4)
594 ui.writenoi18n(
595 b'deltas against prev : ' + fmt % pcfmt(numprev, numdeltas)
596 )
597 if numprev > 0:
598 ui.writenoi18n(
599 b' where prev = p1 : ' + fmt2 % pcfmt(nump1prev, numprev)
600 )
601 ui.writenoi18n(
602 b' where prev = p2 : ' + fmt2 % pcfmt(nump2prev, numprev)
603 )
604 ui.writenoi18n(
605 b' other-ancestor : ' + fmt2 % pcfmt(numoprev, numprev)
606 )
607 ui.writenoi18n(
608 b' unrelated : ' + fmt2 % pcfmt(numoprev, numprev)
609 )
610 if gdelta:
611 ui.writenoi18n(
612 b'deltas against p1 : ' + fmt % pcfmt(nump1, numdeltas)
613 )
614 ui.writenoi18n(
615 b'deltas against p2 : ' + fmt % pcfmt(nump2, numdeltas)
616 )
617 ui.writenoi18n(
618 b'deltas against ancs : '
619 + fmt % pcfmt(num_other_ancestors, numdeltas)
620 )
621 ui.writenoi18n(
622 b'deltas against other : '
623 + fmt % pcfmt(numother_nad, numdeltas)
624 )
625
626
627 def debug_delta_find(ui, revlog, rev, base_rev=nodemod.nullrev):
628 """display the search process for a delta"""
629 deltacomputer = deltautil.deltacomputer(
630 revlog,
631 write_debug=ui.write,
632 debug_search=not ui.quiet,
633 )
634
635 node = revlog.node(rev)
636 p1r, p2r = revlog.parentrevs(rev)
637 p1 = revlog.node(p1r)
638 p2 = revlog.node(p2r)
639 full_text = revlog.revision(rev)
640 btext = [full_text]
641 textlen = len(btext[0])
642 cachedelta = None
643 flags = revlog.flags(rev)
644
645 if base_rev != nodemod.nullrev:
646 base_text = revlog.revision(base_rev)
647 delta = mdiff.textdiff(base_text, full_text)
648
649 cachedelta = (base_rev, delta, constants.DELTA_BASE_REUSE_TRY)
650 btext = [None]
651
652 revinfo = revlogutils.revisioninfo(
653 node,
654 p1,
655 p2,
656 btext,
657 textlen,
658 cachedelta,
659 flags,
660 )
661
662 fh = revlog._datafp()
663 deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev)
664
665
666 def _get_revlogs(repo, changelog: bool, manifest: bool, filelogs: bool):
667 """yield revlogs from this repository"""
668 if changelog:
669 yield repo.changelog
670
671 if manifest:
672 # XXX: Handle tree manifest
673 root_mf = repo.manifestlog.getstorage(b'')
674 assert not root_mf._treeondisk
675 yield root_mf._revlog
676
677 if filelogs:
678 files = set()
679 for rev in repo:
680 ctx = repo[rev]
681 files |= set(ctx.files())
682
683 for f in sorted(files):
684 yield repo.file(f)._revlog
685
686
687 def debug_revlog_stats(
688 repo, fm, changelog: bool, manifest: bool, filelogs: bool
689 ):
690 """Format revlog statistics for debugging purposes
691
692 fm: the output formatter.
693 """
694 fm.plain(b'rev-count data-size inl type target \n')
695
696 for rlog in _get_revlogs(repo, changelog, manifest, filelogs):
697 fm.startitem()
698 nb_rev = len(rlog)
699 inline = rlog._inline
700 data_size = rlog._get_data_offset(nb_rev - 1)
701
702 target = rlog.target
703 revlog_type = b'unknown'
704 revlog_target = b''
705 if target[0] == constants.KIND_CHANGELOG:
706 revlog_type = b'changelog'
707 elif target[0] == constants.KIND_MANIFESTLOG:
708 revlog_type = b'manifest'
709 revlog_target = target[1]
710 elif target[0] == constants.KIND_FILELOG:
711 revlog_type = b'file'
712 revlog_target = target[1]
713
714 fm.write(b'revlog.rev-count', b'%9d', nb_rev)
715 fm.write(b'revlog.data-size', b'%12d', data_size)
716
717 fm.write(b'revlog.inline', b' %-3s', b'yes' if inline else b'no')
718 fm.write(b'revlog.type', b' %-9s', revlog_type)
719 fm.write(b'revlog.target', b' %s', revlog_target)
720
721 fm.plain(b'\n')
@@ -20,6 +20,8 b' from .constants import ('
20 20 COMP_MODE_DEFAULT,
21 21 COMP_MODE_INLINE,
22 22 COMP_MODE_PLAIN,
23 DELTA_BASE_REUSE_FORCE,
24 DELTA_BASE_REUSE_NO,
23 25 KIND_CHANGELOG,
24 26 KIND_FILELOG,
25 27 KIND_MANIFESTLOG,
@@ -576,13 +578,20 b' def drop_u_compression(delta):'
576 578 )
577 579
578 580
579 def isgooddeltainfo(revlog, deltainfo, revinfo):
581 def is_good_delta_info(revlog, deltainfo, revinfo):
580 582 """Returns True if the given delta is good. Good means that it is within
581 583 the disk span, disk size, and chain length bounds that we know to be
582 584 performant."""
583 585 if deltainfo is None:
584 586 return False
585 587
588 if (
589 revinfo.cachedelta is not None
590 and deltainfo.base == revinfo.cachedelta[0]
591 and revinfo.cachedelta[2] == DELTA_BASE_REUSE_FORCE
592 ):
593 return True
594
586 595 # - 'deltainfo.distance' is the distance from the base revision --
587 596 # bounding it limits the amount of I/O we need to do.
588 597 # - 'deltainfo.compresseddeltalen' is the sum of the total size of
@@ -655,7 +664,16 b' def isgooddeltainfo(revlog, deltainfo, r'
655 664 LIMIT_BASE2TEXT = 500
656 665
657 666
658 def _candidategroups(revlog, textlen, p1, p2, cachedelta):
667 def _candidategroups(
668 revlog,
669 textlen,
670 p1,
671 p2,
672 cachedelta,
673 excluded_bases=None,
674 target_rev=None,
675 snapshot_cache=None,
676 ):
659 677 """Provides group of revision to be tested as delta base
660 678
661 679 This top level function focus on emitting groups with unique and worthwhile
@@ -666,15 +684,31 b' def _candidategroups(revlog, textlen, p1'
666 684 yield None
667 685 return
668 686
687 if (
688 cachedelta is not None
689 and nullrev == cachedelta[0]
690 and cachedelta[2] == DELTA_BASE_REUSE_FORCE
691 ):
692 # instruction are to forcibly do a full snapshot
693 yield None
694 return
695
669 696 deltalength = revlog.length
670 697 deltaparent = revlog.deltaparent
671 698 sparse = revlog._sparserevlog
672 699 good = None
673 700
674 701 deltas_limit = textlen * LIMIT_DELTA2TEXT
702 group_chunk_size = revlog._candidate_group_chunk_size
675 703
676 704 tested = {nullrev}
677 candidates = _refinedgroups(revlog, p1, p2, cachedelta)
705 candidates = _refinedgroups(
706 revlog,
707 p1,
708 p2,
709 cachedelta,
710 snapshot_cache=snapshot_cache,
711 )
678 712 while True:
679 713 temptative = candidates.send(good)
680 714 if temptative is None:
@@ -694,15 +728,37 b' def _candidategroups(revlog, textlen, p1'
694 728 # filter out revision we tested already
695 729 if rev in tested:
696 730 continue
697 tested.add(rev)
731
732 if (
733 cachedelta is not None
734 and rev == cachedelta[0]
735 and cachedelta[2] == DELTA_BASE_REUSE_FORCE
736 ):
737 # instructions are to forcibly consider/use this delta base
738 group.append(rev)
739 continue
740
741 # an higher authority deamed the base unworthy (e.g. censored)
742 if excluded_bases is not None and rev in excluded_bases:
743 tested.add(rev)
744 continue
745 # We are in some recomputation cases and that rev is too high in
746 # the revlog
747 if target_rev is not None and rev >= target_rev:
748 tested.add(rev)
749 continue
698 750 # filter out delta base that will never produce good delta
699 751 if deltas_limit < revlog.length(rev):
752 tested.add(rev)
700 753 continue
701 754 if sparse and revlog.rawsize(rev) < (textlen // LIMIT_BASE2TEXT):
755 tested.add(rev)
702 756 continue
703 757 # no delta for rawtext-changing revs (see "candelta" for why)
704 758 if revlog.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS:
759 tested.add(rev)
705 760 continue
761
706 762 # If we reach here, we are about to build and test a delta.
707 763 # The delta building process will compute the chaininfo in all
708 764 # case, since that computation is cached, it is fine to access it
@@ -710,9 +766,11 b' def _candidategroups(revlog, textlen, p1'
710 766 chainlen, chainsize = revlog._chaininfo(rev)
711 767 # if chain will be too long, skip base
712 768 if revlog._maxchainlen and chainlen >= revlog._maxchainlen:
769 tested.add(rev)
713 770 continue
714 771 # if chain already have too much data, skip base
715 772 if deltas_limit < chainsize:
773 tested.add(rev)
716 774 continue
717 775 if sparse and revlog.upperboundcomp is not None:
718 776 maxcomp = revlog.upperboundcomp
@@ -731,36 +789,46 b' def _candidategroups(revlog, textlen, p1'
731 789 snapshotlimit = textlen >> snapshotdepth
732 790 if snapshotlimit < lowestrealisticdeltalen:
733 791 # delta lower bound is larger than accepted upper bound
792 tested.add(rev)
734 793 continue
735 794
736 795 # check the relative constraint on the delta size
737 796 revlength = revlog.length(rev)
738 797 if revlength < lowestrealisticdeltalen:
739 798 # delta probable lower bound is larger than target base
799 tested.add(rev)
740 800 continue
741 801
742 802 group.append(rev)
743 803 if group:
744 # XXX: in the sparse revlog case, group can become large,
745 # impacting performances. Some bounding or slicing mecanism
746 # would help to reduce this impact.
747 good = yield tuple(group)
804 # When the size of the candidate group is big, it can result in a
805 # quite significant performance impact. To reduce this, we can send
806 # them in smaller batches until the new batch does not provide any
807 # improvements.
808 #
809 # This might reduce the overall efficiency of the compression in
810 # some corner cases, but that should also prevent very pathological
811 # cases from being an issue. (eg. 20 000 candidates).
812 #
813 # XXX note that the ordering of the group becomes important as it
814 # now impacts the final result. The current order is unprocessed
815 # and can be improved.
816 if group_chunk_size == 0:
817 tested.update(group)
818 good = yield tuple(group)
819 else:
820 prev_good = good
821 for start in range(0, len(group), group_chunk_size):
822 sub_group = group[start : start + group_chunk_size]
823 tested.update(sub_group)
824 good = yield tuple(sub_group)
825 if prev_good == good:
826 break
827
748 828 yield None
749 829
750 830
751 def _findsnapshots(revlog, cache, start_rev):
752 """find snapshot from start_rev to tip"""
753 if util.safehasattr(revlog.index, b'findsnapshots'):
754 revlog.index.findsnapshots(cache, start_rev)
755 else:
756 deltaparent = revlog.deltaparent
757 issnapshot = revlog.issnapshot
758 for rev in revlog.revs(start_rev):
759 if issnapshot(rev):
760 cache[deltaparent(rev)].append(rev)
761
762
763 def _refinedgroups(revlog, p1, p2, cachedelta):
831 def _refinedgroups(revlog, p1, p2, cachedelta, snapshot_cache=None):
764 832 good = None
765 833 # First we try to reuse a the delta contained in the bundle.
766 834 # (or from the source revlog)
@@ -768,15 +836,28 b' def _refinedgroups(revlog, p1, p2, cache'
768 836 # This logic only applies to general delta repositories and can be disabled
769 837 # through configuration. Disabling reuse source delta is useful when
770 838 # we want to make sure we recomputed "optimal" deltas.
771 if cachedelta and revlog._generaldelta and revlog._lazydeltabase:
839 debug_info = None
840 if cachedelta is not None and cachedelta[2] > DELTA_BASE_REUSE_NO:
772 841 # Assume what we received from the server is a good choice
773 842 # build delta will reuse the cache
843 if debug_info is not None:
844 debug_info['cached-delta.tested'] += 1
774 845 good = yield (cachedelta[0],)
775 846 if good is not None:
847 if debug_info is not None:
848 debug_info['cached-delta.accepted'] += 1
776 849 yield None
777 850 return
778 snapshots = collections.defaultdict(list)
779 for candidates in _rawgroups(revlog, p1, p2, cachedelta, snapshots):
851 if snapshot_cache is None:
852 snapshot_cache = SnapshotCache()
853 groups = _rawgroups(
854 revlog,
855 p1,
856 p2,
857 cachedelta,
858 snapshot_cache,
859 )
860 for candidates in groups:
780 861 good = yield candidates
781 862 if good is not None:
782 863 break
@@ -797,19 +878,22 b' def _refinedgroups(revlog, p1, p2, cache'
797 878 break
798 879 good = yield (base,)
799 880 # refine snapshot up
800 if not snapshots:
801 _findsnapshots(revlog, snapshots, good + 1)
881 if not snapshot_cache.snapshots:
882 snapshot_cache.update(revlog, good + 1)
802 883 previous = None
803 884 while good != previous:
804 885 previous = good
805 children = tuple(sorted(c for c in snapshots[good]))
886 children = tuple(sorted(c for c in snapshot_cache.snapshots[good]))
806 887 good = yield children
807 888
808 # we have found nothing
889 if debug_info is not None:
890 if good is None:
891 debug_info['no-solution'] += 1
892
809 893 yield None
810 894
811 895
812 def _rawgroups(revlog, p1, p2, cachedelta, snapshots=None):
896 def _rawgroups(revlog, p1, p2, cachedelta, snapshot_cache=None):
813 897 """Provides group of revision to be tested as delta base
814 898
815 899 This lower level function focus on emitting delta theorically interresting
@@ -840,9 +924,9 b' def _rawgroups(revlog, p1, p2, cachedelt'
840 924 yield parents
841 925
842 926 if sparse and parents:
843 if snapshots is None:
844 # map: base-rev: snapshot-rev
845 snapshots = collections.defaultdict(list)
927 if snapshot_cache is None:
928 # map: base-rev: [snapshot-revs]
929 snapshot_cache = SnapshotCache()
846 930 # See if we can use an existing snapshot in the parent chains to use as
847 931 # a base for a new intermediate-snapshot
848 932 #
@@ -856,7 +940,7 b' def _rawgroups(revlog, p1, p2, cachedelt'
856 940 break
857 941 parents_snaps[idx].add(s)
858 942 snapfloor = min(parents_snaps[0]) + 1
859 _findsnapshots(revlog, snapshots, snapfloor)
943 snapshot_cache.update(revlog, snapfloor)
860 944 # search for the highest "unrelated" revision
861 945 #
862 946 # Adding snapshots used by "unrelated" revision increase the odd we
@@ -879,14 +963,14 b' def _rawgroups(revlog, p1, p2, cachedelt'
879 963 # chain.
880 964 max_depth = max(parents_snaps.keys())
881 965 chain = deltachain(other)
882 for idx, s in enumerate(chain):
966 for depth, s in enumerate(chain):
883 967 if s < snapfloor:
884 968 continue
885 if max_depth < idx:
969 if max_depth < depth:
886 970 break
887 971 if not revlog.issnapshot(s):
888 972 break
889 parents_snaps[idx].add(s)
973 parents_snaps[depth].add(s)
890 974 # Test them as possible intermediate snapshot base
891 975 # We test them from highest to lowest level. High level one are more
892 976 # likely to result in small delta
@@ -894,7 +978,7 b' def _rawgroups(revlog, p1, p2, cachedelt'
894 978 for idx, snaps in sorted(parents_snaps.items(), reverse=True):
895 979 siblings = set()
896 980 for s in snaps:
897 siblings.update(snapshots[s])
981 siblings.update(snapshot_cache.snapshots[s])
898 982 # Before considering making a new intermediate snapshot, we check
899 983 # if an existing snapshot, children of base we consider, would be
900 984 # suitable.
@@ -922,7 +1006,8 b' def _rawgroups(revlog, p1, p2, cachedelt'
922 1006 # revisions instead of starting our own. Without such re-use,
923 1007 # topological branches would keep reopening new full chains. Creating
924 1008 # more and more snapshot as the repository grow.
925 yield tuple(snapshots[nullrev])
1009 full = [r for r in snapshot_cache.snapshots[nullrev] if snapfloor <= r]
1010 yield tuple(sorted(full))
926 1011
927 1012 if not sparse:
928 1013 # other approach failed try against prev to hopefully save us a
@@ -930,11 +1015,74 b' def _rawgroups(revlog, p1, p2, cachedelt'
930 1015 yield (prev,)
931 1016
932 1017
1018 class SnapshotCache:
1019 __slots__ = ('snapshots', '_start_rev', '_end_rev')
1020
1021 def __init__(self):
1022 self.snapshots = collections.defaultdict(set)
1023 self._start_rev = None
1024 self._end_rev = None
1025
1026 def update(self, revlog, start_rev=0):
1027 """find snapshots from start_rev to tip"""
1028 nb_revs = len(revlog)
1029 end_rev = nb_revs - 1
1030 if start_rev > end_rev:
1031 return # range is empty
1032
1033 if self._start_rev is None:
1034 assert self._end_rev is None
1035 self._update(revlog, start_rev, end_rev)
1036 elif not (self._start_rev <= start_rev and end_rev <= self._end_rev):
1037 if start_rev < self._start_rev:
1038 self._update(revlog, start_rev, self._start_rev - 1)
1039 if self._end_rev < end_rev:
1040 self._update(revlog, self._end_rev + 1, end_rev)
1041
1042 if self._start_rev is None:
1043 assert self._end_rev is None
1044 self._end_rev = end_rev
1045 self._start_rev = start_rev
1046 else:
1047 self._start_rev = min(self._start_rev, start_rev)
1048 self._end_rev = max(self._end_rev, end_rev)
1049 assert self._start_rev <= self._end_rev, (
1050 self._start_rev,
1051 self._end_rev,
1052 )
1053
1054 def _update(self, revlog, start_rev, end_rev):
1055 """internal method that actually do update content"""
1056 assert self._start_rev is None or (
1057 start_rev < self._start_rev or start_rev > self._end_rev
1058 ), (self._start_rev, self._end_rev, start_rev, end_rev)
1059 assert self._start_rev is None or (
1060 end_rev < self._start_rev or end_rev > self._end_rev
1061 ), (self._start_rev, self._end_rev, start_rev, end_rev)
1062 cache = self.snapshots
1063 if util.safehasattr(revlog.index, b'findsnapshots'):
1064 revlog.index.findsnapshots(cache, start_rev, end_rev)
1065 else:
1066 deltaparent = revlog.deltaparent
1067 issnapshot = revlog.issnapshot
1068 for rev in revlog.revs(start_rev, end_rev):
1069 if issnapshot(rev):
1070 cache[deltaparent(rev)].add(rev)
1071
1072
933 1073 class deltacomputer:
934 def __init__(self, revlog, write_debug=None, debug_search=False):
1074 def __init__(
1075 self,
1076 revlog,
1077 write_debug=None,
1078 debug_search=False,
1079 debug_info=None,
1080 ):
935 1081 self.revlog = revlog
936 1082 self._write_debug = write_debug
937 1083 self._debug_search = debug_search
1084 self._debug_info = debug_info
1085 self._snapshot_cache = SnapshotCache()
938 1086
939 1087 def buildtext(self, revinfo, fh):
940 1088 """Builds a fulltext version of a revision
@@ -998,7 +1146,7 b' class deltacomputer:'
998 1146 snapshotdepth = len(revlog._deltachain(deltabase)[0])
999 1147 delta = None
1000 1148 if revinfo.cachedelta:
1001 cachebase, cachediff = revinfo.cachedelta
1149 cachebase = revinfo.cachedelta[0]
1002 1150 # check if the diff still apply
1003 1151 currentbase = cachebase
1004 1152 while (
@@ -1103,11 +1251,14 b' class deltacomputer:'
1103 1251 if revinfo.flags & REVIDX_RAWTEXT_CHANGING_FLAGS:
1104 1252 return self._fullsnapshotinfo(fh, revinfo, target_rev)
1105 1253
1106 if self._write_debug is not None:
1254 gather_debug = (
1255 self._write_debug is not None or self._debug_info is not None
1256 )
1257 debug_search = self._write_debug is not None and self._debug_search
1258
1259 if gather_debug:
1107 1260 start = util.timer()
1108 1261
1109 debug_search = self._write_debug is not None and self._debug_search
1110
1111 1262 # count the number of different delta we tried (for debug purpose)
1112 1263 dbg_try_count = 0
1113 1264 # count the number of "search round" we did. (for debug purpose)
@@ -1122,7 +1273,7 b' class deltacomputer:'
1122 1273 deltainfo = None
1123 1274 p1r, p2r = revlog.rev(p1), revlog.rev(p2)
1124 1275
1125 if self._write_debug is not None:
1276 if gather_debug:
1126 1277 if p1r != nullrev:
1127 1278 p1_chain_len = revlog._chaininfo(p1r)[0]
1128 1279 else:
@@ -1137,7 +1288,14 b' class deltacomputer:'
1137 1288 self._write_debug(msg)
1138 1289
1139 1290 groups = _candidategroups(
1140 self.revlog, revinfo.textlen, p1r, p2r, cachedelta
1291 self.revlog,
1292 revinfo.textlen,
1293 p1r,
1294 p2r,
1295 cachedelta,
1296 excluded_bases,
1297 target_rev,
1298 snapshot_cache=self._snapshot_cache,
1141 1299 )
1142 1300 candidaterevs = next(groups)
1143 1301 while candidaterevs is not None:
@@ -1147,7 +1305,13 b' class deltacomputer:'
1147 1305 if deltainfo is not None:
1148 1306 prev = deltainfo.base
1149 1307
1150 if p1 in candidaterevs or p2 in candidaterevs:
1308 if (
1309 cachedelta is not None
1310 and len(candidaterevs) == 1
1311 and cachedelta[0] in candidaterevs
1312 ):
1313 round_type = b"cached-delta"
1314 elif p1 in candidaterevs or p2 in candidaterevs:
1151 1315 round_type = b"parents"
1152 1316 elif prev is not None and all(c < prev for c in candidaterevs):
1153 1317 round_type = b"refine-down"
@@ -1195,16 +1359,7 b' class deltacomputer:'
1195 1359 msg = b"DBG-DELTAS-SEARCH: base=%d\n"
1196 1360 msg %= self.revlog.deltaparent(candidaterev)
1197 1361 self._write_debug(msg)
1198 if candidaterev in excluded_bases:
1199 if debug_search:
1200 msg = b"DBG-DELTAS-SEARCH: EXCLUDED\n"
1201 self._write_debug(msg)
1202 continue
1203 if candidaterev >= target_rev:
1204 if debug_search:
1205 msg = b"DBG-DELTAS-SEARCH: TOO-HIGH\n"
1206 self._write_debug(msg)
1207 continue
1362
1208 1363 dbg_try_count += 1
1209 1364
1210 1365 if debug_search:
@@ -1216,7 +1371,7 b' class deltacomputer:'
1216 1371 msg %= delta_end - delta_start
1217 1372 self._write_debug(msg)
1218 1373 if candidatedelta is not None:
1219 if isgooddeltainfo(self.revlog, candidatedelta, revinfo):
1374 if is_good_delta_info(self.revlog, candidatedelta, revinfo):
1220 1375 if debug_search:
1221 1376 msg = b"DBG-DELTAS-SEARCH: DELTA: length=%d (GOOD)\n"
1222 1377 msg %= candidatedelta.deltalen
@@ -1244,12 +1399,28 b' class deltacomputer:'
1244 1399 else:
1245 1400 dbg_type = b"delta"
1246 1401
1247 if self._write_debug is not None:
1402 if gather_debug:
1248 1403 end = util.timer()
1404 if dbg_type == b'full':
1405 used_cached = (
1406 cachedelta is not None
1407 and dbg_try_rounds == 0
1408 and dbg_try_count == 0
1409 and cachedelta[0] == nullrev
1410 )
1411 else:
1412 used_cached = (
1413 cachedelta is not None
1414 and dbg_try_rounds == 1
1415 and dbg_try_count == 1
1416 and deltainfo.base == cachedelta[0]
1417 )
1249 1418 dbg = {
1250 1419 'duration': end - start,
1251 1420 'revision': target_rev,
1421 'delta-base': deltainfo.base, # pytype: disable=attribute-error
1252 1422 'search_round_count': dbg_try_rounds,
1423 'using-cached-base': used_cached,
1253 1424 'delta_try_count': dbg_try_count,
1254 1425 'type': dbg_type,
1255 1426 'p1-chain-len': p1_chain_len,
@@ -1279,31 +1450,39 b' class deltacomputer:'
1279 1450 target_revlog += b'%s:' % target_key
1280 1451 dbg['target-revlog'] = target_revlog
1281 1452
1282 msg = (
1283 b"DBG-DELTAS:"
1284 b" %-12s"
1285 b" rev=%d:"
1286 b" search-rounds=%d"
1287 b" try-count=%d"
1288 b" - delta-type=%-6s"
1289 b" snap-depth=%d"
1290 b" - p1-chain-length=%d"
1291 b" p2-chain-length=%d"
1292 b" - duration=%f"
1293 b"\n"
1294 )
1295 msg %= (
1296 dbg["target-revlog"],
1297 dbg["revision"],
1298 dbg["search_round_count"],
1299 dbg["delta_try_count"],
1300 dbg["type"],
1301 dbg["snapshot-depth"],
1302 dbg["p1-chain-len"],
1303 dbg["p2-chain-len"],
1304 dbg["duration"],
1305 )
1306 self._write_debug(msg)
1453 if self._debug_info is not None:
1454 self._debug_info.append(dbg)
1455
1456 if self._write_debug is not None:
1457 msg = (
1458 b"DBG-DELTAS:"
1459 b" %-12s"
1460 b" rev=%d:"
1461 b" delta-base=%d"
1462 b" is-cached=%d"
1463 b" - search-rounds=%d"
1464 b" try-count=%d"
1465 b" - delta-type=%-6s"
1466 b" snap-depth=%d"
1467 b" - p1-chain-length=%d"
1468 b" p2-chain-length=%d"
1469 b" - duration=%f"
1470 b"\n"
1471 )
1472 msg %= (
1473 dbg["target-revlog"],
1474 dbg["revision"],
1475 dbg["delta-base"],
1476 dbg["using-cached-base"],
1477 dbg["search_round_count"],
1478 dbg["delta_try_count"],
1479 dbg["type"],
1480 dbg["snapshot-depth"],
1481 dbg["p1-chain-len"],
1482 dbg["p2-chain-len"],
1483 dbg["duration"],
1484 )
1485 self._write_debug(msg)
1307 1486 return deltainfo
1308 1487
1309 1488
@@ -90,7 +90,7 b' if stable_docket_file:'
90 90 # * 8 bytes: pending size of data
91 91 # * 8 bytes: pending size of sidedata
92 92 # * 1 bytes: default compression header
93 S_HEADER = struct.Struct(constants.INDEX_HEADER_FMT + b'BBBBBBLLLLLLc')
93 S_HEADER = struct.Struct(constants.INDEX_HEADER_FMT + b'BBBBBBQQQQQQc')
94 94 # * 1 bytes: size of index uuid
95 95 # * 8 bytes: size of file
96 96 S_OLD_UID = struct.Struct('>BL')
@@ -1868,13 +1868,12 b' def outgoing(repo, subset, x):'
1868 1868 dests = []
1869 1869 missing = set()
1870 1870 for path in urlutil.get_push_paths(repo, repo.ui, dests):
1871 dest = path.pushloc or path.loc
1872 1871 branches = path.branch, []
1873 1872
1874 1873 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1875 1874 if revs:
1876 1875 revs = [repo.lookup(rev) for rev in revs]
1877 other = hg.peer(repo, {}, dest)
1876 other = hg.peer(repo, {}, path)
1878 1877 try:
1879 1878 with repo.ui.silent():
1880 1879 outgoing = discovery.findcommonoutgoing(
@@ -2130,11 +2129,9 b' def remote(repo, subset, x):'
2130 2129 dest = getstring(l[1], _(b"remote requires a repository path"))
2131 2130 if not dest:
2132 2131 dest = b'default'
2133 dest, branches = urlutil.get_unique_pull_path(
2134 b'remote', repo, repo.ui, dest
2135 )
2136
2137 other = hg.peer(repo, {}, dest)
2132 path = urlutil.get_unique_pull_path_obj(b'remote', repo.ui, dest)
2133
2134 other = hg.peer(repo, {}, path)
2138 2135 n = other.lookup(q)
2139 2136 if n in repo:
2140 2137 r = repo[n].rev()
@@ -4,6 +4,11 b' import fcntl'
4 4 import os
5 5 import sys
6 6
7 from typing import (
8 List,
9 Tuple,
10 )
11
7 12 from .pycompat import getattr
8 13 from . import (
9 14 encoding,
@@ -11,6 +16,9 b' from . import ('
11 16 util,
12 17 )
13 18
19 if pycompat.TYPE_CHECKING:
20 from . import ui as uimod
21
14 22 # BSD 'more' escapes ANSI color sequences by default. This can be disabled by
15 23 # $MORE variable, but there's no compatible option with Linux 'more'. Given
16 24 # OS X is widely used and most modern Unix systems would have 'less', setting
@@ -18,7 +26,7 b' from . import ('
18 26 fallbackpager = b'less'
19 27
20 28
21 def _rcfiles(path):
29 def _rcfiles(path: bytes) -> List[bytes]:
22 30 rcs = [os.path.join(path, b'hgrc')]
23 31 rcdir = os.path.join(path, b'hgrc.d')
24 32 try:
@@ -34,7 +42,7 b' def _rcfiles(path):'
34 42 return rcs
35 43
36 44
37 def systemrcpath():
45 def systemrcpath() -> List[bytes]:
38 46 path = []
39 47 if pycompat.sysplatform == b'plan9':
40 48 root = b'lib/mercurial'
@@ -49,7 +57,7 b' def systemrcpath():'
49 57 return path
50 58
51 59
52 def userrcpath():
60 def userrcpath() -> List[bytes]:
53 61 if pycompat.sysplatform == b'plan9':
54 62 return [encoding.environ[b'home'] + b'/lib/hgrc']
55 63 elif pycompat.isdarwin:
@@ -65,7 +73,7 b' def userrcpath():'
65 73 ]
66 74
67 75
68 def termsize(ui):
76 def termsize(ui: "uimod.ui") -> Tuple[int, int]:
69 77 try:
70 78 import termios
71 79
@@ -88,7 +96,7 b' def termsize(ui):'
88 96 except ValueError:
89 97 pass
90 98 except IOError as e:
91 if e[0] == errno.EINVAL: # pytype: disable=unsupported-operands
99 if e.errno == errno.EINVAL:
92 100 pass
93 101 else:
94 102 raise
@@ -1219,7 +1219,7 b' def cleanupnodes('
1219 1219 )
1220 1220
1221 1221
1222 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1222 def addremove(repo, matcher, prefix, uipathfn, opts=None, open_tr=None):
1223 1223 if opts is None:
1224 1224 opts = {}
1225 1225 m = matcher
@@ -1279,7 +1279,9 b' def addremove(repo, matcher, prefix, uip'
1279 1279 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1280 1280 )
1281 1281
1282 if not dry_run:
1282 if not dry_run and (unknown or forgotten or deleted or renames):
1283 if open_tr is not None:
1284 open_tr()
1283 1285 _markchanges(repo, unknown + forgotten, deleted, renames)
1284 1286
1285 1287 for f in rejected:
@@ -1863,7 +1865,12 b' def gdinitconfig(ui):'
1863 1865
1864 1866
1865 1867 def gddeltaconfig(ui):
1866 """helper function to know if incoming delta should be optimised"""
1868 """helper function to know if incoming deltas should be optimized
1869
1870 The `format.generaldelta` config is an old form of the config that also
1871 implies that incoming delta-bases should be never be trusted. This function
1872 exists for this purpose.
1873 """
1867 1874 # experimental config: format.generaldelta
1868 1875 return ui.configbool(b'format', b'generaldelta')
1869 1876
@@ -1,4 +1,10 b''
1 1 import os
2 import winreg # pytype: disable=import-error
3
4 from typing import (
5 List,
6 Tuple,
7 )
2 8
3 9 from . import (
4 10 encoding,
@@ -7,19 +13,14 b' from . import ('
7 13 win32,
8 14 )
9 15
10 try:
11 import _winreg as winreg # pytype: disable=import-error
12
13 winreg.CloseKey
14 except ImportError:
15 # py2 only
16 import winreg # pytype: disable=import-error
16 if pycompat.TYPE_CHECKING:
17 from . import ui as uimod
17 18
18 19 # MS-DOS 'more' is the only pager available by default on Windows.
19 20 fallbackpager = b'more'
20 21
21 22
22 def systemrcpath():
23 def systemrcpath() -> List[bytes]:
23 24 '''return default os-specific hgrc search path'''
24 25 rcpath = []
25 26 filename = win32.executablepath()
@@ -27,7 +28,7 b' def systemrcpath():'
27 28 progrc = os.path.join(os.path.dirname(filename), b'mercurial.ini')
28 29 rcpath.append(progrc)
29 30
30 def _processdir(progrcd):
31 def _processdir(progrcd: bytes) -> None:
31 32 if os.path.isdir(progrcd):
32 33 for f, kind in sorted(util.listdir(progrcd)):
33 34 if f.endswith(b'.rc'):
@@ -68,7 +69,7 b' def systemrcpath():'
68 69 return rcpath
69 70
70 71
71 def userrcpath():
72 def userrcpath() -> List[bytes]:
72 73 '''return os-specific hgrc search path to the user dir'''
73 74 home = _legacy_expanduser(b'~')
74 75 path = [os.path.join(home, b'mercurial.ini'), os.path.join(home, b'.hgrc')]
@@ -79,7 +80,7 b' def userrcpath():'
79 80 return path
80 81
81 82
82 def _legacy_expanduser(path):
83 def _legacy_expanduser(path: bytes) -> bytes:
83 84 """Expand ~ and ~user constructs in the pre 3.8 style"""
84 85
85 86 # Python 3.8+ changed the expansion of '~' from HOME to USERPROFILE. See
@@ -111,5 +112,5 b' def _legacy_expanduser(path):'
111 112 return userhome + path[i:]
112 113
113 114
114 def termsize(ui):
115 def termsize(ui: "uimod.ui") -> Tuple[int, int]:
115 116 return win32.termsize()
@@ -247,6 +247,14 b' class Shelf:'
247 247 for ext in shelvefileextensions:
248 248 self.vfs.tryunlink(self.name + b'.' + ext)
249 249
250 def changed_files(self, ui, repo):
251 try:
252 ctx = repo.unfiltered()[self.readinfo()[b'node']]
253 return ctx.files()
254 except (FileNotFoundError, error.RepoLookupError):
255 filename = self.vfs.join(self.name + b'.patch')
256 return patch.changedfiles(ui, repo, filename)
257
250 258
251 259 def _optimized_match(repo, node):
252 260 """
@@ -424,10 +432,26 b' def _restoreactivebookmark(repo, mark):'
424 432
425 433 def _aborttransaction(repo, tr):
426 434 """Abort current transaction for shelve/unshelve, but keep dirstate"""
427 dirstatebackupname = b'dirstate.shelve'
428 repo.dirstate.savebackup(None, dirstatebackupname)
429 tr.abort()
430 repo.dirstate.restorebackup(None, dirstatebackupname)
435 # disable the transaction invalidation of the dirstate, to preserve the
436 # current change in memory.
437 ds = repo.dirstate
438 # The assert below check that nobody else did such wrapping.
439 #
440 # These is not such other wrapping currently, but if someone try to
441 # implement one in the future, this will explicitly break here instead of
442 # misbehaving in subtle ways.
443 assert 'invalidate' not in vars(ds)
444 try:
445 # note : we could simply disable the transaction abort callback, but
446 # other code also tries to rollback and invalidate this.
447 ds.invalidate = lambda: None
448 tr.abort()
449 finally:
450 del ds.invalidate
451 # manually write the change in memory since we can no longer rely on the
452 # transaction to do so.
453 assert repo.currenttransaction() is None
454 repo.dirstate.write(None)
431 455
432 456
433 457 def getshelvename(repo, parent, opts):
@@ -599,7 +623,8 b' def _docreatecmd(ui, repo, pats, opts):'
599 623 activebookmark = _backupactivebookmark(repo)
600 624 extra = {b'internal': b'shelve'}
601 625 if includeunknown:
602 _includeunknownfiles(repo, pats, opts, extra)
626 with repo.dirstate.changing_files(repo):
627 _includeunknownfiles(repo, pats, opts, extra)
603 628
604 629 if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
605 630 # In non-bare shelve we don't store newly created branch
@@ -629,7 +654,7 b' def _docreatecmd(ui, repo, pats, opts):'
629 654
630 655 ui.status(_(b'shelved as %s\n') % name)
631 656 if opts[b'keep']:
632 with repo.dirstate.parentchange():
657 with repo.dirstate.changing_parents(repo):
633 658 scmutil.movedirstate(repo, parent, match)
634 659 else:
635 660 hg.update(repo, parent.node())
@@ -854,18 +879,18 b' def unshelvecontinue(ui, repo, state, op'
854 879 shelvectx = repo[state.parents[1]]
855 880 pendingctx = state.pendingctx
856 881
857 with repo.dirstate.parentchange():
882 with repo.dirstate.changing_parents(repo):
858 883 repo.setparents(state.pendingctx.node(), repo.nullid)
859 884 repo.dirstate.write(repo.currenttransaction())
860 885
861 886 targetphase = _target_phase(repo)
862 887 overrides = {(b'phases', b'new-commit'): targetphase}
863 888 with repo.ui.configoverride(overrides, b'unshelve'):
864 with repo.dirstate.parentchange():
889 with repo.dirstate.changing_parents(repo):
865 890 repo.setparents(state.parents[0], repo.nullid)
866 newnode, ispartialunshelve = _createunshelvectx(
867 ui, repo, shelvectx, basename, interactive, opts
868 )
891 newnode, ispartialunshelve = _createunshelvectx(
892 ui, repo, shelvectx, basename, interactive, opts
893 )
869 894
870 895 if newnode is None:
871 896 shelvectx = state.pendingctx
@@ -1060,11 +1085,11 b' def _rebaserestoredcommit('
1060 1085 )
1061 1086 raise error.ConflictResolutionRequired(b'unshelve')
1062 1087
1063 with repo.dirstate.parentchange():
1088 with repo.dirstate.changing_parents(repo):
1064 1089 repo.setparents(tmpwctx.node(), repo.nullid)
1065 newnode, ispartialunshelve = _createunshelvectx(
1066 ui, repo, shelvectx, basename, interactive, opts
1067 )
1090 newnode, ispartialunshelve = _createunshelvectx(
1091 ui, repo, shelvectx, basename, interactive, opts
1092 )
1068 1093
1069 1094 if newnode is None:
1070 1095 shelvectx = tmpwctx
@@ -1210,7 +1235,8 b' def _dounshelve(ui, repo, basename, opts'
1210 1235 restorebranch(ui, repo, branchtorestore)
1211 1236 shelvedstate.clear(repo)
1212 1237 _finishunshelve(repo, oldtiprev, tr, activebookmark)
1213 _forgetunknownfiles(repo, shelvectx, addedbefore)
1238 with repo.dirstate.changing_files(repo):
1239 _forgetunknownfiles(repo, shelvectx, addedbefore)
1214 1240 if not ispartialunshelve:
1215 1241 unshelvecleanup(ui, repo, basename, opts)
1216 1242 finally:
@@ -512,6 +512,8 b' def simplemerge('
512 512 conflicts = False
513 513 if mode == b'union':
514 514 lines = _resolve(m3, (1, 2))
515 elif mode == b'union-other-first':
516 lines = _resolve(m3, (2, 1))
515 517 elif mode == b'local':
516 518 lines = _resolve(m3, (1,))
517 519 elif mode == b'other':
@@ -451,7 +451,7 b' def filterupdatesactions(repo, wctx, mct'
451 451 message,
452 452 )
453 453
454 with repo.dirstate.parentchange():
454 with repo.dirstate.changing_parents(repo):
455 455 mergemod.applyupdates(
456 456 repo,
457 457 tmresult,
@@ -655,7 +655,7 b' def clearrules(repo, force=False):'
655 655 The remaining sparse config only has profiles, if defined. The working
656 656 directory is refreshed, as needed.
657 657 """
658 with repo.wlock(), repo.dirstate.parentchange():
658 with repo.wlock(), repo.dirstate.changing_parents(repo):
659 659 raw = repo.vfs.tryread(b'sparse')
660 660 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
661 661
@@ -671,7 +671,7 b' def importfromfiles(repo, opts, paths, f'
671 671 The updated sparse config is written out and the working directory
672 672 is refreshed, as needed.
673 673 """
674 with repo.wlock(), repo.dirstate.parentchange():
674 with repo.wlock(), repo.dirstate.changing_parents(repo):
675 675 # read current configuration
676 676 raw = repo.vfs.tryread(b'sparse')
677 677 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
@@ -730,7 +730,7 b' def updateconfig('
730 730
731 731 The new config is written out and a working directory refresh is performed.
732 732 """
733 with repo.wlock(), repo.lock(), repo.dirstate.parentchange():
733 with repo.wlock(), repo.lock(), repo.dirstate.changing_parents(repo):
734 734 raw = repo.vfs.tryread(b'sparse')
735 735 oldinclude, oldexclude, oldprofiles = parseconfig(
736 736 repo.ui, raw, b'sparse'
@@ -372,7 +372,7 b' def _performhandshake(ui, stdin, stdout,'
372 372
373 373 class sshv1peer(wireprotov1peer.wirepeer):
374 374 def __init__(
375 self, ui, url, proc, stdin, stdout, stderr, caps, autoreadstderr=True
375 self, ui, path, proc, stdin, stdout, stderr, caps, autoreadstderr=True
376 376 ):
377 377 """Create a peer from an existing SSH connection.
378 378
@@ -383,8 +383,7 b' class sshv1peer(wireprotov1peer.wirepeer'
383 383 ``autoreadstderr`` denotes whether to automatically read from
384 384 stderr and to forward its output.
385 385 """
386 self._url = url
387 self.ui = ui
386 super().__init__(ui, path=path)
388 387 # self._subprocess is unused. Keeping a handle on the process
389 388 # holds a reference and prevents it from being garbage collected.
390 389 self._subprocess = proc
@@ -411,14 +410,11 b' class sshv1peer(wireprotov1peer.wirepeer'
411 410 # Begin of ipeerconnection interface.
412 411
413 412 def url(self):
414 return self._url
413 return self.path.loc
415 414
416 415 def local(self):
417 416 return None
418 417
419 def peer(self):
420 return self
421
422 418 def canpush(self):
423 419 return True
424 420
@@ -610,16 +606,16 b' def makepeer(ui, path, proc, stdin, stdo'
610 606 )
611 607
612 608
613 def instance(ui, path, create, intents=None, createopts=None):
609 def make_peer(ui, path, create, intents=None, createopts=None):
614 610 """Create an SSH peer.
615 611
616 612 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
617 613 """
618 u = urlutil.url(path, parsequery=False, parsefragment=False)
614 u = urlutil.url(path.loc, parsequery=False, parsefragment=False)
619 615 if u.scheme != b'ssh' or not u.host or u.path is None:
620 616 raise error.RepoError(_(b"couldn't parse location %s") % path)
621 617
622 urlutil.checksafessh(path)
618 urlutil.checksafessh(path.loc)
623 619
624 620 if u.passwd is not None:
625 621 raise error.RepoError(_(b'password in URL not supported'))
@@ -225,6 +225,7 b' class statichttprepository('
225 225 self.encodepats = None
226 226 self.decodepats = None
227 227 self._transref = None
228 self._dirstate = None
228 229
229 230 def _restrictcapabilities(self, caps):
230 231 caps = super(statichttprepository, self)._restrictcapabilities(caps)
@@ -236,8 +237,8 b' class statichttprepository('
236 237 def local(self):
237 238 return False
238 239
239 def peer(self):
240 return statichttppeer(self)
240 def peer(self, path=None):
241 return statichttppeer(self, path=path)
241 242
242 243 def wlock(self, wait=True):
243 244 raise error.LockUnavailable(
@@ -259,7 +260,8 b' class statichttprepository('
259 260 pass # statichttprepository are read only
260 261
261 262
262 def instance(ui, path, create, intents=None, createopts=None):
263 def make_peer(ui, path, create, intents=None, createopts=None):
263 264 if create:
264 265 raise error.Abort(_(b'cannot create new static-http repository'))
265 return statichttprepository(ui, path[7:])
266 url = path.loc[7:]
267 return statichttprepository(ui, url).peer(path=path)
@@ -1049,7 +1049,7 b' def main(argv=None):'
1049 1049 # process options
1050 1050 try:
1051 1051 opts, args = pycompat.getoptb(
1052 sys.argv[optstart:],
1052 pycompat.sysargv[optstart:],
1053 1053 b"hl:f:o:p:",
1054 1054 [b"help", b"limit=", b"file=", b"output-file=", b"script-path="],
1055 1055 )
@@ -241,31 +241,32 b' def debugstrip(ui, repo, *revs, **opts):'
241 241
242 242 revs = sorted(rootnodes)
243 243 if update and opts.get(b'keep'):
244 urev = _findupdatetarget(repo, revs)
245 uctx = repo[urev]
244 with repo.dirstate.changing_parents(repo):
245 urev = _findupdatetarget(repo, revs)
246 uctx = repo[urev]
246 247
247 # only reset the dirstate for files that would actually change
248 # between the working context and uctx
249 descendantrevs = repo.revs(b"only(., %d)", uctx.rev())
250 changedfiles = []
251 for rev in descendantrevs:
252 # blindly reset the files, regardless of what actually changed
253 changedfiles.extend(repo[rev].files())
248 # only reset the dirstate for files that would actually change
249 # between the working context and uctx
250 descendantrevs = repo.revs(b"only(., %d)", uctx.rev())
251 changedfiles = []
252 for rev in descendantrevs:
253 # blindly reset the files, regardless of what actually changed
254 changedfiles.extend(repo[rev].files())
254 255
255 # reset files that only changed in the dirstate too
256 dirstate = repo.dirstate
257 dirchanges = [
258 f for f in dirstate if not dirstate.get_entry(f).maybe_clean
259 ]
260 changedfiles.extend(dirchanges)
256 # reset files that only changed in the dirstate too
257 dirstate = repo.dirstate
258 dirchanges = [
259 f for f in dirstate if not dirstate.get_entry(f).maybe_clean
260 ]
261 changedfiles.extend(dirchanges)
261 262
262 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
263 repo.dirstate.write(repo.currenttransaction())
263 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
264 repo.dirstate.write(repo.currenttransaction())
264 265
265 # clear resolve state
266 mergestatemod.mergestate.clean(repo)
266 # clear resolve state
267 mergestatemod.mergestate.clean(repo)
267 268
268 update = False
269 update = False
269 270
270 271 strip(
271 272 ui,
@@ -569,9 +569,20 b' class hgsubrepo(abstractsubrepo):'
569 569
570 570 @annotatesubrepoerror
571 571 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
572 return cmdutil.add(
573 ui, self._repo, match, prefix, uipathfn, explicitonly, **opts
574 )
572 # XXX Ideally, we could let the caller take the `changing_files`
573 # context. However this is not an abstraction that make sense for
574 # other repository types, and leaking that details purely related to
575 # dirstate seems unfortunate. So for now the context will be used here.
576 with self._repo.wlock(), self._repo.dirstate.changing_files(self._repo):
577 return cmdutil.add(
578 ui,
579 self._repo,
580 match,
581 prefix,
582 uipathfn,
583 explicitonly,
584 **opts,
585 )
575 586
576 587 @annotatesubrepoerror
577 588 def addremove(self, m, prefix, uipathfn, opts):
@@ -580,7 +591,18 b' class hgsubrepo(abstractsubrepo):'
580 591 # be used to process sibling subrepos however.
581 592 opts = copy.copy(opts)
582 593 opts[b'subrepos'] = True
583 return scmutil.addremove(self._repo, m, prefix, uipathfn, opts)
594 # XXX Ideally, we could let the caller take the `changing_files`
595 # context. However this is not an abstraction that make sense for
596 # other repository types, and leaking that details purely related to
597 # dirstate seems unfortunate. So for now the context will be used here.
598 with self._repo.wlock(), self._repo.dirstate.changing_files(self._repo):
599 return scmutil.addremove(
600 self._repo,
601 m,
602 prefix,
603 uipathfn,
604 opts,
605 )
584 606
585 607 @annotatesubrepoerror
586 608 def cat(self, match, fm, fntemplate, prefix, **opts):
@@ -621,7 +643,7 b' class hgsubrepo(abstractsubrepo):'
621 643 match,
622 644 prefix=prefix,
623 645 listsubrepos=True,
624 **opts
646 **opts,
625 647 )
626 648 except error.RepoLookupError as inst:
627 649 self.ui.warn(
@@ -946,16 +968,21 b' class hgsubrepo(abstractsubrepo):'
946 968
947 969 @annotatesubrepoerror
948 970 def forget(self, match, prefix, uipathfn, dryrun, interactive):
949 return cmdutil.forget(
950 self.ui,
951 self._repo,
952 match,
953 prefix,
954 uipathfn,
955 True,
956 dryrun=dryrun,
957 interactive=interactive,
958 )
971 # XXX Ideally, we could let the caller take the `changing_files`
972 # context. However this is not an abstraction that make sense for
973 # other repository types, and leaking that details purely related to
974 # dirstate seems unfortunate. So for now the context will be used here.
975 with self._repo.wlock(), self._repo.dirstate.changing_files(self._repo):
976 return cmdutil.forget(
977 self.ui,
978 self._repo,
979 match,
980 prefix,
981 uipathfn,
982 True,
983 dryrun=dryrun,
984 interactive=interactive,
985 )
959 986
960 987 @annotatesubrepoerror
961 988 def removefiles(
@@ -969,17 +996,22 b' class hgsubrepo(abstractsubrepo):'
969 996 dryrun,
970 997 warnings,
971 998 ):
972 return cmdutil.remove(
973 self.ui,
974 self._repo,
975 matcher,
976 prefix,
977 uipathfn,
978 after,
979 force,
980 subrepos,
981 dryrun,
982 )
999 # XXX Ideally, we could let the caller take the `changing_files`
1000 # context. However this is not an abstraction that make sense for
1001 # other repository types, and leaking that details purely related to
1002 # dirstate seems unfortunate. So for now the context will be used here.
1003 with self._repo.wlock(), self._repo.dirstate.changing_files(self._repo):
1004 return cmdutil.remove(
1005 self.ui,
1006 self._repo,
1007 matcher,
1008 prefix,
1009 uipathfn,
1010 after,
1011 force,
1012 subrepos,
1013 dryrun,
1014 )
983 1015
984 1016 @annotatesubrepoerror
985 1017 def revert(self, substate, *pats, **opts):
@@ -1009,7 +1041,12 b' class hgsubrepo(abstractsubrepo):'
1009 1041 pats = [b'set:modified()']
1010 1042 else:
1011 1043 pats = []
1012 cmdutil.revert(self.ui, self._repo, ctx, *pats, **opts)
1044 # XXX Ideally, we could let the caller take the `changing_files`
1045 # context. However this is not an abstraction that make sense for
1046 # other repository types, and leaking that details purely related to
1047 # dirstate seems unfortunate. So for now the context will be used here.
1048 with self._repo.wlock(), self._repo.dirstate.changing_files(self._repo):
1049 cmdutil.revert(self.ui, self._repo, ctx, *pats, **opts)
1013 1050
1014 1051 def shortid(self, revid):
1015 1052 return revid[:12]
@@ -1123,7 +1160,7 b' class svnsubrepo(abstractsubrepo):'
1123 1160 stdout=subprocess.PIPE,
1124 1161 stderr=subprocess.PIPE,
1125 1162 env=procutil.tonativeenv(env),
1126 **extrakw
1163 **extrakw,
1127 1164 )
1128 1165 stdout, stderr = map(util.fromnativeeol, p.communicate())
1129 1166 stderr = stderr.strip()
@@ -1488,7 +1525,7 b' class gitsubrepo(abstractsubrepo):'
1488 1525 close_fds=procutil.closefds,
1489 1526 stdout=subprocess.PIPE,
1490 1527 stderr=errpipe,
1491 **extrakw
1528 **extrakw,
1492 1529 )
1493 1530 if stream:
1494 1531 return p.stdout, None
@@ -664,8 +664,9 b' def _tag('
664 664
665 665 repo.invalidatecaches()
666 666
667 if b'.hgtags' not in repo.dirstate:
668 repo[None].add([b'.hgtags'])
667 with repo.dirstate.changing_files(repo):
668 if b'.hgtags' not in repo.dirstate:
669 repo[None].add([b'.hgtags'])
669 670
670 671 m = matchmod.exact([b'.hgtags'])
671 672 tagnode = repo.commit(
@@ -177,10 +177,17 b' def tokenize(program, start, end, term=N'
177 177 quote = program[pos : pos + 2]
178 178 s = pos = pos + 2
179 179 while pos < end: # find closing escaped quote
180 # pycompat.bytestr (and bytes) both have .startswith() that
181 # takes an optional start and an optional end, but pytype thinks
182 # it only takes 2 args.
183
184 # pytype: disable=wrong-arg-count
180 185 if program.startswith(b'\\\\\\', pos, end):
181 186 pos += 4 # skip over double escaped characters
182 187 continue
183 188 if program.startswith(quote, pos, end):
189 # pytype: enable=wrong-arg-count
190
184 191 # interpret as if it were a part of an outer string
185 192 data = parser.unescapestr(program[s:pos])
186 193 if token == b'template':
@@ -300,7 +307,14 b' def _scantemplate(tmpl, start, stop, quo'
300 307 return
301 308
302 309 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
310
311 # pycompat.bytestr (and bytes) both have .startswith() that
312 # takes an optional start and an optional end, but pytype thinks
313 # it only takes 2 args.
314
315 # pytype: disable=wrong-arg-count
303 316 if not tmpl.startswith(b'}', pos):
317 # pytype: enable=wrong-arg-count
304 318 raise error.ParseError(_(b"invalid token"), pos)
305 319 yield (b'template', parseres, n)
306 320 pos += 1
@@ -1,6 +1,6 b''
1 1 The MIT License (MIT)
2 2
3 Copyright (c) 2015 Hynek Schlawack
3 Copyright (c) 2015 Hynek Schlawack and the attrs contributors
4 4
5 5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 6 of this software and associated documentation files (the "Software"), to deal
@@ -1,37 +1,35 b''
1 from __future__ import absolute_import, division, print_function
1 # SPDX-License-Identifier: MIT
2
3
4 import sys
5
6 from functools import partial
2 7
3 from ._funcs import (
4 asdict,
5 assoc,
6 astuple,
7 evolve,
8 has,
9 )
8 from . import converters, exceptions, filters, setters, validators
9 from ._cmp import cmp_using
10 from ._config import get_run_validators, set_run_validators
11 from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types
10 12 from ._make import (
13 NOTHING,
11 14 Attribute,
12 15 Factory,
13 NOTHING,
14 attr,
15 attributes,
16 attrib,
17 attrs,
16 18 fields,
19 fields_dict,
17 20 make_class,
18 21 validate,
19 22 )
20 from ._config import (
21 get_run_validators,
22 set_run_validators,
23 )
24 from . import exceptions
25 from . import filters
26 from . import converters
27 from . import validators
23 from ._version_info import VersionInfo
28 24
29 25
30 __version__ = "17.2.0"
26 __version__ = "22.1.0"
27 __version_info__ = VersionInfo._from_version_string(__version__)
31 28
32 29 __title__ = "attrs"
33 30 __description__ = "Classes Without Boilerplate"
34 __uri__ = "http://www.attrs.org/"
31 __url__ = "https://www.attrs.org/"
32 __uri__ = __url__
35 33 __doc__ = __description__ + " <" + __uri__ + ">"
36 34
37 35 __author__ = "Hynek Schlawack"
@@ -41,8 +39,9 b' from . import validators'
41 39 __copyright__ = "Copyright (c) 2015 Hynek Schlawack"
42 40
43 41
44 s = attrs = attributes
45 ib = attrib = attr
42 s = attributes = attrs
43 ib = attr = attrib
44 dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)
46 45
47 46 __all__ = [
48 47 "Attribute",
@@ -55,17 +54,26 b' ib = attrib = attr'
55 54 "attrib",
56 55 "attributes",
57 56 "attrs",
57 "cmp_using",
58 58 "converters",
59 59 "evolve",
60 60 "exceptions",
61 61 "fields",
62 "fields_dict",
62 63 "filters",
63 64 "get_run_validators",
64 65 "has",
65 66 "ib",
66 67 "make_class",
68 "resolve_types",
67 69 "s",
68 70 "set_run_validators",
71 "setters",
69 72 "validate",
70 73 "validators",
71 74 ]
75
76 if sys.version_info[:2] >= (3, 6):
77 from ._next_gen import define, field, frozen, mutable # noqa: F401
78
79 __all__.extend(("define", "field", "frozen", "mutable"))
@@ -1,90 +1,185 b''
1 from __future__ import absolute_import, division, print_function
1 # SPDX-License-Identifier: MIT
2
3
4 import inspect
5 import platform
6 import sys
7 import threading
8 import types
9 import warnings
10
11 from collections.abc import Mapping, Sequence # noqa
12
13
14 PYPY = platform.python_implementation() == "PyPy"
15 PY36 = sys.version_info[:2] >= (3, 6)
16 HAS_F_STRINGS = PY36
17 PY310 = sys.version_info[:2] >= (3, 10)
2 18
3 import sys
4 import types
19
20 if PYPY or PY36:
21 ordered_dict = dict
22 else:
23 from collections import OrderedDict
24
25 ordered_dict = OrderedDict
26
27
28 def just_warn(*args, **kw):
29 warnings.warn(
30 "Running interpreter doesn't sufficiently support code object "
31 "introspection. Some features like bare super() or accessing "
32 "__class__ will not work with slotted classes.",
33 RuntimeWarning,
34 stacklevel=2,
35 )
5 36
6 37
7 PY2 = sys.version_info[0] == 2
38 class _AnnotationExtractor:
39 """
40 Extract type annotations from a callable, returning None whenever there
41 is none.
42 """
43
44 __slots__ = ["sig"]
45
46 def __init__(self, callable):
47 try:
48 self.sig = inspect.signature(callable)
49 except (ValueError, TypeError): # inspect failed
50 self.sig = None
51
52 def get_first_param_type(self):
53 """
54 Return the type annotation of the first argument if it's not empty.
55 """
56 if not self.sig:
57 return None
58
59 params = list(self.sig.parameters.values())
60 if params and params[0].annotation is not inspect.Parameter.empty:
61 return params[0].annotation
62
63 return None
64
65 def get_return_type(self):
66 """
67 Return the return type if it's not empty.
68 """
69 if (
70 self.sig
71 and self.sig.return_annotation is not inspect.Signature.empty
72 ):
73 return self.sig.return_annotation
74
75 return None
8 76
9 77
10 if PY2:
11 from UserDict import IterableUserDict
12
13 # We 'bundle' isclass instead of using inspect as importing inspect is
14 # fairly expensive (order of 10-15 ms for a modern machine in 2016)
15 def isclass(klass):
16 return isinstance(klass, (type, types.ClassType))
78 def make_set_closure_cell():
79 """Return a function of two arguments (cell, value) which sets
80 the value stored in the closure cell `cell` to `value`.
81 """
82 # pypy makes this easy. (It also supports the logic below, but
83 # why not do the easy/fast thing?)
84 if PYPY:
17 85
18 # TYPE is used in exceptions, repr(int) is different on Python 2 and 3.
19 TYPE = "type"
86 def set_closure_cell(cell, value):
87 cell.__setstate__((value,))
88
89 return set_closure_cell
20 90
21 def iteritems(d):
22 return d.iteritems()
91 # Otherwise gotta do it the hard way.
23 92
24 def iterkeys(d):
25 return d.iterkeys()
93 # Create a function that will set its first cellvar to `value`.
94 def set_first_cellvar_to(value):
95 x = value
96 return
26 97
27 # Python 2 is bereft of a read-only dict proxy, so we make one!
28 class ReadOnlyDict(IterableUserDict):
29 """
30 Best-effort read-only dict wrapper.
31 """
98 # This function will be eliminated as dead code, but
99 # not before its reference to `x` forces `x` to be
100 # represented as a closure cell rather than a local.
101 def force_x_to_be_a_cell(): # pragma: no cover
102 return x
32 103
33 def __setitem__(self, key, val):
34 # We gently pretend we're a Python 3 mappingproxy.
35 raise TypeError("'mappingproxy' object does not support item "
36 "assignment")
104 try:
105 # Extract the code object and make sure our assumptions about
106 # the closure behavior are correct.
107 co = set_first_cellvar_to.__code__
108 if co.co_cellvars != ("x",) or co.co_freevars != ():
109 raise AssertionError # pragma: no cover
37 110
38 def update(self, _):
39 # We gently pretend we're a Python 3 mappingproxy.
40 raise AttributeError("'mappingproxy' object has no attribute "
41 "'update'")
111 # Convert this code object to a code object that sets the
112 # function's first _freevar_ (not cellvar) to the argument.
113 if sys.version_info >= (3, 8):
42 114
43 def __delitem__(self, _):
44 # We gently pretend we're a Python 3 mappingproxy.
45 raise TypeError("'mappingproxy' object does not support item "
46 "deletion")
115 def set_closure_cell(cell, value):
116 cell.cell_contents = value
47 117
48 def clear(self):
49 # We gently pretend we're a Python 3 mappingproxy.
50 raise AttributeError("'mappingproxy' object has no attribute "
51 "'clear'")
52
53 def pop(self, key, default=None):
54 # We gently pretend we're a Python 3 mappingproxy.
55 raise AttributeError("'mappingproxy' object has no attribute "
56 "'pop'")
118 else:
119 args = [co.co_argcount]
120 args.append(co.co_kwonlyargcount)
121 args.extend(
122 [
123 co.co_nlocals,
124 co.co_stacksize,
125 co.co_flags,
126 co.co_code,
127 co.co_consts,
128 co.co_names,
129 co.co_varnames,
130 co.co_filename,
131 co.co_name,
132 co.co_firstlineno,
133 co.co_lnotab,
134 # These two arguments are reversed:
135 co.co_cellvars,
136 co.co_freevars,
137 ]
138 )
139 set_first_freevar_code = types.CodeType(*args)
57 140
58 def popitem(self):
59 # We gently pretend we're a Python 3 mappingproxy.
60 raise AttributeError("'mappingproxy' object has no attribute "
61 "'popitem'")
62
63 def setdefault(self, key, default=None):
64 # We gently pretend we're a Python 3 mappingproxy.
65 raise AttributeError("'mappingproxy' object has no attribute "
66 "'setdefault'")
141 def set_closure_cell(cell, value):
142 # Create a function using the set_first_freevar_code,
143 # whose first closure cell is `cell`. Calling it will
144 # change the value of that cell.
145 setter = types.FunctionType(
146 set_first_freevar_code, {}, "setter", (), (cell,)
147 )
148 # And call it to set the cell.
149 setter(value)
67 150
68 def __repr__(self):
69 # Override to be identical to the Python 3 version.
70 return "mappingproxy(" + repr(self.data) + ")"
151 # Make sure it works on this interpreter:
152 def make_func_with_cell():
153 x = None
154
155 def func():
156 return x # pragma: no cover
71 157
72 def metadata_proxy(d):
73 res = ReadOnlyDict()
74 res.data.update(d) # We blocked update, so we have to do it like this.
75 return res
158 return func
159
160 cell = make_func_with_cell().__closure__[0]
161 set_closure_cell(cell, 100)
162 if cell.cell_contents != 100:
163 raise AssertionError # pragma: no cover
76 164
77 else:
78 def isclass(klass):
79 return isinstance(klass, type)
165 except Exception:
166 return just_warn
167 else:
168 return set_closure_cell
80 169
81 TYPE = "class"
170
171 set_closure_cell = make_set_closure_cell()
82 172
83 def iteritems(d):
84 return d.items()
85
86 def iterkeys(d):
87 return d.keys()
88
89 def metadata_proxy(d):
90 return types.MappingProxyType(dict(d))
173 # Thread-local global to track attrs instances which are already being repr'd.
174 # This is needed because there is no other (thread-safe) way to pass info
175 # about the instances that are already being repr'd through the call stack
176 # in order to ensure we don't perform infinite recursion.
177 #
178 # For instance, if an instance contains a dict which contains that instance,
179 # we need to know that we're already repr'ing the outside instance from within
180 # the dict's repr() call.
181 #
182 # This lives here rather than in _make.py so that the functions in _make.py
183 # don't have a direct reference to the thread-local in their globals dict.
184 # If they have such a reference, it breaks cloudpickle.
185 repr_context = threading.local()
@@ -1,4 +1,4 b''
1 from __future__ import absolute_import, division, print_function
1 # SPDX-License-Identifier: MIT
2 2
3 3
4 4 __all__ = ["set_run_validators", "get_run_validators"]
@@ -9,6 +9,10 b' from __future__ import absolute_import, '
9 9 def set_run_validators(run):
10 10 """
11 11 Set whether or not validators are run. By default, they are run.
12
13 .. deprecated:: 21.3.0 It will not be removed, but it also will not be
14 moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()`
15 instead.
12 16 """
13 17 if not isinstance(run, bool):
14 18 raise TypeError("'run' must be bool.")
@@ -19,5 +23,9 b' def set_run_validators(run):'
19 23 def get_run_validators():
20 24 """
21 25 Return whether or not validators are run.
26
27 .. deprecated:: 21.3.0 It will not be removed, but it also will not be
28 moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()`
29 instead.
22 30 """
23 31 return _run_validators
@@ -1,14 +1,20 b''
1 from __future__ import absolute_import, division, print_function
1 # SPDX-License-Identifier: MIT
2
2 3
3 4 import copy
4 5
5 from ._compat import iteritems
6 from ._make import NOTHING, fields, _obj_setattr
6 from ._make import NOTHING, _obj_setattr, fields
7 7 from .exceptions import AttrsAttributeNotFoundError
8 8
9 9
10 def asdict(inst, recurse=True, filter=None, dict_factory=dict,
11 retain_collection_types=False):
10 def asdict(
11 inst,
12 recurse=True,
13 filter=None,
14 dict_factory=dict,
15 retain_collection_types=False,
16 value_serializer=None,
17 ):
12 18 """
13 19 Return the ``attrs`` attribute values of *inst* as a dict.
14 20
@@ -17,9 +23,9 b' def asdict(inst, recurse=True, filter=No'
17 23 :param inst: Instance of an ``attrs``-decorated class.
18 24 :param bool recurse: Recurse into classes that are also
19 25 ``attrs``-decorated.
20 :param callable filter: A callable whose return code deteremines whether an
26 :param callable filter: A callable whose return code determines whether an
21 27 attribute or element is included (``True``) or dropped (``False``). Is
22 called with the :class:`attr.Attribute` as the first argument and the
28 called with the `attrs.Attribute` as the first argument and the
23 29 value as the second argument.
24 30 :param callable dict_factory: A callable to produce dictionaries from. For
25 31 example, to produce ordered dictionaries instead of normal Python
@@ -27,6 +33,10 b' def asdict(inst, recurse=True, filter=No'
27 33 :param bool retain_collection_types: Do not convert to ``list`` when
28 34 encountering an attribute whose type is ``tuple`` or ``set``. Only
29 35 meaningful if ``recurse`` is ``True``.
36 :param Optional[callable] value_serializer: A hook that is called for every
37 attribute or dict key/value. It receives the current instance, field
38 and value and must return the (updated) value. The hook is run *after*
39 the optional *filter* has been applied.
30 40
31 41 :rtype: return type of *dict_factory*
32 42
@@ -35,6 +45,9 b' def asdict(inst, recurse=True, filter=No'
35 45
36 46 .. versionadded:: 16.0.0 *dict_factory*
37 47 .. versionadded:: 16.1.0 *retain_collection_types*
48 .. versionadded:: 20.3.0 *value_serializer*
49 .. versionadded:: 21.3.0 If a dict has a collection for a key, it is
50 serialized as a tuple.
38 51 """
39 52 attrs = fields(inst.__class__)
40 53 rv = dict_factory()
@@ -42,24 +55,58 b' def asdict(inst, recurse=True, filter=No'
42 55 v = getattr(inst, a.name)
43 56 if filter is not None and not filter(a, v):
44 57 continue
58
59 if value_serializer is not None:
60 v = value_serializer(inst, a, v)
61
45 62 if recurse is True:
46 63 if has(v.__class__):
47 rv[a.name] = asdict(v, recurse=True, filter=filter,
48 dict_factory=dict_factory)
49 elif isinstance(v, (tuple, list, set)):
64 rv[a.name] = asdict(
65 v,
66 recurse=True,
67 filter=filter,
68 dict_factory=dict_factory,
69 retain_collection_types=retain_collection_types,
70 value_serializer=value_serializer,
71 )
72 elif isinstance(v, (tuple, list, set, frozenset)):
50 73 cf = v.__class__ if retain_collection_types is True else list
51 rv[a.name] = cf([
52 asdict(i, recurse=True, filter=filter,
53 dict_factory=dict_factory)
54 if has(i.__class__) else i
55 for i in v
56 ])
74 rv[a.name] = cf(
75 [
76 _asdict_anything(
77 i,
78 is_key=False,
79 filter=filter,
80 dict_factory=dict_factory,
81 retain_collection_types=retain_collection_types,
82 value_serializer=value_serializer,
83 )
84 for i in v
85 ]
86 )
57 87 elif isinstance(v, dict):
58 88 df = dict_factory
59 rv[a.name] = df((
60 asdict(kk, dict_factory=df) if has(kk.__class__) else kk,
61 asdict(vv, dict_factory=df) if has(vv.__class__) else vv)
62 for kk, vv in iteritems(v))
89 rv[a.name] = df(
90 (
91 _asdict_anything(
92 kk,
93 is_key=True,
94 filter=filter,
95 dict_factory=df,
96 retain_collection_types=retain_collection_types,
97 value_serializer=value_serializer,
98 ),
99 _asdict_anything(
100 vv,
101 is_key=False,
102 filter=filter,
103 dict_factory=df,
104 retain_collection_types=retain_collection_types,
105 value_serializer=value_serializer,
106 ),
107 )
108 for kk, vv in v.items()
109 )
63 110 else:
64 111 rv[a.name] = v
65 112 else:
@@ -67,8 +114,86 b' def asdict(inst, recurse=True, filter=No'
67 114 return rv
68 115
69 116
70 def astuple(inst, recurse=True, filter=None, tuple_factory=tuple,
71 retain_collection_types=False):
117 def _asdict_anything(
118 val,
119 is_key,
120 filter,
121 dict_factory,
122 retain_collection_types,
123 value_serializer,
124 ):
125 """
126 ``asdict`` only works on attrs instances, this works on anything.
127 """
128 if getattr(val.__class__, "__attrs_attrs__", None) is not None:
129 # Attrs class.
130 rv = asdict(
131 val,
132 recurse=True,
133 filter=filter,
134 dict_factory=dict_factory,
135 retain_collection_types=retain_collection_types,
136 value_serializer=value_serializer,
137 )
138 elif isinstance(val, (tuple, list, set, frozenset)):
139 if retain_collection_types is True:
140 cf = val.__class__
141 elif is_key:
142 cf = tuple
143 else:
144 cf = list
145
146 rv = cf(
147 [
148 _asdict_anything(
149 i,
150 is_key=False,
151 filter=filter,
152 dict_factory=dict_factory,
153 retain_collection_types=retain_collection_types,
154 value_serializer=value_serializer,
155 )
156 for i in val
157 ]
158 )
159 elif isinstance(val, dict):
160 df = dict_factory
161 rv = df(
162 (
163 _asdict_anything(
164 kk,
165 is_key=True,
166 filter=filter,
167 dict_factory=df,
168 retain_collection_types=retain_collection_types,
169 value_serializer=value_serializer,
170 ),
171 _asdict_anything(
172 vv,
173 is_key=False,
174 filter=filter,
175 dict_factory=df,
176 retain_collection_types=retain_collection_types,
177 value_serializer=value_serializer,
178 ),
179 )
180 for kk, vv in val.items()
181 )
182 else:
183 rv = val
184 if value_serializer is not None:
185 rv = value_serializer(None, None, rv)
186
187 return rv
188
189
190 def astuple(
191 inst,
192 recurse=True,
193 filter=None,
194 tuple_factory=tuple,
195 retain_collection_types=False,
196 ):
72 197 """
73 198 Return the ``attrs`` attribute values of *inst* as a tuple.
74 199
@@ -79,7 +204,7 b' def astuple(inst, recurse=True, filter=N'
79 204 ``attrs``-decorated.
80 205 :param callable filter: A callable whose return code determines whether an
81 206 attribute or element is included (``True``) or dropped (``False``). Is
82 called with the :class:`attr.Attribute` as the first argument and the
207 called with the `attrs.Attribute` as the first argument and the
83 208 value as the second argument.
84 209 :param callable tuple_factory: A callable to produce tuples from. For
85 210 example, to produce lists instead of tuples.
@@ -104,38 +229,61 b' def astuple(inst, recurse=True, filter=N'
104 229 continue
105 230 if recurse is True:
106 231 if has(v.__class__):
107 rv.append(astuple(v, recurse=True, filter=filter,
108 tuple_factory=tuple_factory,
109 retain_collection_types=retain))
110 elif isinstance(v, (tuple, list, set)):
232 rv.append(
233 astuple(
234 v,
235 recurse=True,
236 filter=filter,
237 tuple_factory=tuple_factory,
238 retain_collection_types=retain,
239 )
240 )
241 elif isinstance(v, (tuple, list, set, frozenset)):
111 242 cf = v.__class__ if retain is True else list
112 rv.append(cf([
113 astuple(j, recurse=True, filter=filter,
114 tuple_factory=tuple_factory,
115 retain_collection_types=retain)
116 if has(j.__class__) else j
117 for j in v
118 ]))
243 rv.append(
244 cf(
245 [
246 astuple(
247 j,
248 recurse=True,
249 filter=filter,
250 tuple_factory=tuple_factory,
251 retain_collection_types=retain,
252 )
253 if has(j.__class__)
254 else j
255 for j in v
256 ]
257 )
258 )
119 259 elif isinstance(v, dict):
120 260 df = v.__class__ if retain is True else dict
121 rv.append(df(
261 rv.append(
262 df(
122 263 (
123 264 astuple(
124 265 kk,
125 266 tuple_factory=tuple_factory,
126 retain_collection_types=retain
127 ) if has(kk.__class__) else kk,
267 retain_collection_types=retain,
268 )
269 if has(kk.__class__)
270 else kk,
128 271 astuple(
129 272 vv,
130 273 tuple_factory=tuple_factory,
131 retain_collection_types=retain
132 ) if has(vv.__class__) else vv
274 retain_collection_types=retain,
275 )
276 if has(vv.__class__)
277 else vv,
133 278 )
134 for kk, vv in iteritems(v)))
279 for kk, vv in v.items()
280 )
281 )
135 282 else:
136 283 rv.append(v)
137 284 else:
138 285 rv.append(v)
286
139 287 return rv if tuple_factory is list else tuple_factory(rv)
140 288
141 289
@@ -146,7 +294,7 b' def has(cls):'
146 294 :param type cls: Class to introspect.
147 295 :raise TypeError: If *cls* is not a class.
148 296
149 :rtype: :class:`bool`
297 :rtype: bool
150 298 """
151 299 return getattr(cls, "__attrs_attrs__", None) is not None
152 300
@@ -166,19 +314,26 b' def assoc(inst, **changes):'
166 314 class.
167 315
168 316 .. deprecated:: 17.1.0
169 Use :func:`evolve` instead.
317 Use `attrs.evolve` instead if you can.
318 This function will not be removed du to the slightly different approach
319 compared to `attrs.evolve`.
170 320 """
171 321 import warnings
172 warnings.warn("assoc is deprecated and will be removed after 2018/01.",
173 DeprecationWarning)
322
323 warnings.warn(
324 "assoc is deprecated and will be removed after 2018/01.",
325 DeprecationWarning,
326 stacklevel=2,
327 )
174 328 new = copy.copy(inst)
175 329 attrs = fields(inst.__class__)
176 for k, v in iteritems(changes):
330 for k, v in changes.items():
177 331 a = getattr(attrs, k, NOTHING)
178 332 if a is NOTHING:
179 333 raise AttrsAttributeNotFoundError(
180 "{k} is not an attrs attribute on {cl}."
181 .format(k=k, cl=new.__class__)
334 "{k} is not an attrs attribute on {cl}.".format(
335 k=k, cl=new.__class__
336 )
182 337 )
183 338 _obj_setattr(new, k, v)
184 339 return new
@@ -209,4 +364,57 b' def evolve(inst, **changes):'
209 364 init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
210 365 if init_name not in changes:
211 366 changes[init_name] = getattr(inst, attr_name)
367
212 368 return cls(**changes)
369
370
371 def resolve_types(cls, globalns=None, localns=None, attribs=None):
372 """
373 Resolve any strings and forward annotations in type annotations.
374
375 This is only required if you need concrete types in `Attribute`'s *type*
376 field. In other words, you don't need to resolve your types if you only
377 use them for static type checking.
378
379 With no arguments, names will be looked up in the module in which the class
380 was created. If this is not what you want, e.g. if the name only exists
381 inside a method, you may pass *globalns* or *localns* to specify other
382 dictionaries in which to look up these names. See the docs of
383 `typing.get_type_hints` for more details.
384
385 :param type cls: Class to resolve.
386 :param Optional[dict] globalns: Dictionary containing global variables.
387 :param Optional[dict] localns: Dictionary containing local variables.
388 :param Optional[list] attribs: List of attribs for the given class.
389 This is necessary when calling from inside a ``field_transformer``
390 since *cls* is not an ``attrs`` class yet.
391
392 :raise TypeError: If *cls* is not a class.
393 :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
394 class and you didn't pass any attribs.
395 :raise NameError: If types cannot be resolved because of missing variables.
396
397 :returns: *cls* so you can use this function also as a class decorator.
398 Please note that you have to apply it **after** `attrs.define`. That
399 means the decorator has to come in the line **before** `attrs.define`.
400
401 .. versionadded:: 20.1.0
402 .. versionadded:: 21.1.0 *attribs*
403
404 """
405 # Since calling get_type_hints is expensive we cache whether we've
406 # done it already.
407 if getattr(cls, "__attrs_types_resolved__", None) != cls:
408 import typing
409
410 hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
411 for field in fields(cls) if attribs is None else attribs:
412 if field.name in hints:
413 # Since fields have been frozen we must work around it.
414 _obj_setattr(field, "type", hints[field.name])
415 # We store the class we resolved so that subclasses know they haven't
416 # been resolved.
417 cls.__attrs_types_resolved__ = cls
418
419 # Return the class so you can use it as a decorator too.
420 return cls
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rust/hg-core/src/config.rs to rust/hg-core/src/config/mod.rs
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rust/hg-core/src/revlog.rs to rust/hg-core/src/revlog/mod.rs
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now