1# error.py - Mercurial exceptions
2#
3# Copyright 2005-2008 Olivia Mackall <olivia@selenic.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8"""Mercurial exceptions.
9
10This allows us to catch exceptions at higher levels without forcing
11imports.
12"""
13
14from __future__ import absolute_import
15
16import difflib
17
18# Do not import anything but pycompat here, please
19from . import pycompat
20
21if pycompat.TYPE_CHECKING:
22    from typing import (
23        Any,
24        AnyStr,
25        Iterable,
26        List,
27        Optional,
28        Sequence,
29        Union,
30    )
31
32
33def _tobytes(exc):
34    # type: (...) -> bytes
35    """Byte-stringify exception in the same way as BaseException_str()"""
36    if not exc.args:
37        return b''
38    if len(exc.args) == 1:
39        return pycompat.bytestr(exc.args[0])
40    return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
41
42
43class Hint(object):
44    """Mix-in to provide a hint of an error
45
46    This should come first in the inheritance list to consume a hint and
47    pass remaining arguments to the exception class.
48    """
49
50    def __init__(self, *args, **kw):
51        self.hint = kw.pop('hint', None)  # type: Optional[bytes]
52        super(Hint, self).__init__(*args, **kw)
53
54
55class Error(Hint, Exception):
56    """Base class for Mercurial errors."""
57
58    coarse_exit_code = None
59    detailed_exit_code = None
60
61    def __init__(self, message, hint=None):
62        # type: (bytes, Optional[bytes]) -> None
63        self.message = message
64        self.hint = hint
65        # Pass the message into the Exception constructor to help extensions
66        # that look for exc.args[0].
67        Exception.__init__(self, message)
68
69    def __bytes__(self):
70        return self.message
71
72    if pycompat.ispy3:
73
74        def __str__(self):
75            # type: () -> str
76            # the output would be unreadable if the message was translated,
77            # but do not replace it with encoding.strfromlocal(), which
78            # may raise another exception.
79            return pycompat.sysstr(self.__bytes__())
80
81    def format(self):
82        # type: () -> bytes
83        from .i18n import _
84
85        message = _(b"abort: %s\n") % self.message
86        if self.hint:
87            message += _(b"(%s)\n") % self.hint
88        return message
89
90
91class Abort(Error):
92    """Raised if a command needs to print an error and exit."""
93
94
95class StorageError(Error):
96    """Raised when an error occurs in a storage layer.
97
98    Usually subclassed by a storage-specific exception.
99    """
100
101    detailed_exit_code = 50
102
103
104class RevlogError(StorageError):
105    pass
106
107
108class SidedataHashError(RevlogError):
109    def __init__(self, key, expected, got):
110        # type: (int, bytes, bytes) -> None
111        self.hint = None
112        self.sidedatakey = key
113        self.expecteddigest = expected
114        self.actualdigest = got
115
116
117class FilteredIndexError(IndexError):
118    __bytes__ = _tobytes
119
120
121class LookupError(RevlogError, KeyError):
122    def __init__(self, name, index, message):
123        # type: (bytes, bytes, bytes) -> None
124        self.name = name
125        self.index = index
126        # this can't be called 'message' because at least some installs of
127        # Python 2.6+ complain about the 'message' property being deprecated
128        self.lookupmessage = message
129        if isinstance(name, bytes) and len(name) == 20:
130            from .node import hex
131
132            name = hex(name)
133        # if name is a binary node, it can be None
134        RevlogError.__init__(
135            self, b'%s@%s: %s' % (index, pycompat.bytestr(name), message)
136        )
137
138    def __bytes__(self):
139        return RevlogError.__bytes__(self)
140
141    def __str__(self):
142        return RevlogError.__str__(self)
143
144
145class AmbiguousPrefixLookupError(LookupError):
146    pass
147
148
149class FilteredLookupError(LookupError):
150    pass
151
152
153class ManifestLookupError(LookupError):
154    pass
155
156
157class CommandError(Exception):
158    """Exception raised on errors in parsing the command line."""
159
160    def __init__(self, command, message):
161        # type: (bytes, bytes) -> None
162        self.command = command
163        self.message = message
164        super(CommandError, self).__init__()
165
166    __bytes__ = _tobytes
167
168
169class UnknownCommand(Exception):
170    """Exception raised if command is not in the command table."""
171
172    def __init__(self, command, all_commands=None):
173        # type: (bytes, Optional[List[bytes]]) -> None
174        self.command = command
175        self.all_commands = all_commands
176        super(UnknownCommand, self).__init__()
177
178    __bytes__ = _tobytes
179
180
181class AmbiguousCommand(Exception):
182    """Exception raised if command shortcut matches more than one command."""
183
184    def __init__(self, prefix, matches):
185        # type: (bytes, List[bytes]) -> None
186        self.prefix = prefix
187        self.matches = matches
188        super(AmbiguousCommand, self).__init__()
189
190    __bytes__ = _tobytes
191
192
193class WorkerError(Exception):
194    """Exception raised when a worker process dies."""
195
196    def __init__(self, status_code):
197        # type: (int) -> None
198        self.status_code = status_code
199        # Pass status code to superclass just so it becomes part of __bytes__
200        super(WorkerError, self).__init__(status_code)
201
202    __bytes__ = _tobytes
203
204
205class InterventionRequired(Abort):
206    """Exception raised when a command requires human intervention."""
207
208    coarse_exit_code = 1
209    detailed_exit_code = 240
210
211    def format(self):
212        # type: () -> bytes
213        from .i18n import _
214
215        message = _(b"%s\n") % self.message
216        if self.hint:
217            message += _(b"(%s)\n") % self.hint
218        return message
219
220
221class ConflictResolutionRequired(InterventionRequired):
222    """Exception raised when a continuable command required merge conflict resolution."""
223
224    def __init__(self, opname):
225        # type: (bytes) -> None
226        from .i18n import _
227
228        self.opname = opname
229        InterventionRequired.__init__(
230            self,
231            _(
232                b"unresolved conflicts (see 'hg resolve', then 'hg %s --continue')"
233            )
234            % opname,
235        )
236
237
238class InputError(Abort):
239    """Indicates that the user made an error in their input.
240
241    Examples: Invalid command, invalid flags, invalid revision.
242    """
243
244    detailed_exit_code = 10
245
246
247class StateError(Abort):
248    """Indicates that the operation might work if retried in a different state.
249
250    Examples: Unresolved merge conflicts, unfinished operations.
251    """
252
253    detailed_exit_code = 20
254
255
256class CanceledError(Abort):
257    """Indicates that the user canceled the operation.
258
259    Examples: Close commit editor with error status, quit chistedit.
260    """
261
262    detailed_exit_code = 250
263
264
265class SecurityError(Abort):
266    """Indicates that some aspect of security failed.
267
268    Examples: Bad server credentials, expired local credentials for network
269    filesystem, mismatched GPG signature, DoS protection.
270    """
271
272    detailed_exit_code = 150
273
274
275class HookLoadError(Abort):
276    """raised when loading a hook fails, aborting an operation
277
278    Exists to allow more specialized catching."""
279
280
281class HookAbort(Abort):
282    """raised when a validation hook fails, aborting an operation
283
284    Exists to allow more specialized catching."""
285
286    detailed_exit_code = 40
287
288
289class ConfigError(Abort):
290    """Exception raised when parsing config files"""
291
292    detailed_exit_code = 30
293
294    def __init__(self, message, location=None, hint=None):
295        # type: (bytes, Optional[bytes], Optional[bytes]) -> None
296        super(ConfigError, self).__init__(message, hint=hint)
297        self.location = location
298
299    def format(self):
300        # type: () -> bytes
301        from .i18n import _
302
303        if self.location is not None:
304            message = _(b"config error at %s: %s\n") % (
305                pycompat.bytestr(self.location),
306                self.message,
307            )
308        else:
309            message = _(b"config error: %s\n") % self.message
310        if self.hint:
311            message += _(b"(%s)\n") % self.hint
312        return message
313
314
315class UpdateAbort(Abort):
316    """Raised when an update is aborted for destination issue"""
317
318
319class MergeDestAbort(Abort):
320    """Raised when an update is aborted for destination issues"""
321
322
323class NoMergeDestAbort(MergeDestAbort):
324    """Raised when an update is aborted because there is nothing to merge"""
325
326
327class ManyMergeDestAbort(MergeDestAbort):
328    """Raised when an update is aborted because destination is ambiguous"""
329
330
331class ResponseExpected(Abort):
332    """Raised when an EOF is received for a prompt"""
333
334    def __init__(self):
335        from .i18n import _
336
337        Abort.__init__(self, _(b'response expected'))
338
339
340class RemoteError(Abort):
341    """Exception raised when interacting with a remote repo fails"""
342
343    detailed_exit_code = 100
344
345
346class OutOfBandError(RemoteError):
347    """Exception raised when a remote repo reports failure"""
348
349    def __init__(self, message=None, hint=None):
350        # type: (Optional[bytes], Optional[bytes]) -> None
351        from .i18n import _
352
353        if message:
354            # Abort.format() adds a trailing newline
355            message = _(b"remote error:\n%s") % message.rstrip(b'\n')
356        else:
357            message = _(b"remote error")
358        super(OutOfBandError, self).__init__(message, hint=hint)
359
360
361class ParseError(Abort):
362    """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
363
364    detailed_exit_code = 10
365
366    def __init__(self, message, location=None, hint=None):
367        # type: (bytes, Optional[Union[bytes, int]], Optional[bytes]) -> None
368        super(ParseError, self).__init__(message, hint=hint)
369        self.location = location
370
371    def format(self):
372        # type: () -> bytes
373        from .i18n import _
374
375        if self.location is not None:
376            message = _(b"hg: parse error at %s: %s\n") % (
377                pycompat.bytestr(self.location),
378                self.message,
379            )
380        else:
381            message = _(b"hg: parse error: %s\n") % self.message
382        if self.hint:
383            message += _(b"(%s)\n") % self.hint
384        return message
385
386
387class PatchError(Exception):
388    __bytes__ = _tobytes
389
390
391def getsimilar(symbols, value):
392    # type: (Iterable[bytes], bytes) -> List[bytes]
393    sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
394    # The cutoff for similarity here is pretty arbitrary. It should
395    # probably be investigated and tweaked.
396    return [s for s in symbols if sim(s) > 0.6]
397
398
399def similarity_hint(similar):
400    # type: (List[bytes]) -> Optional[bytes]
401    from .i18n import _
402
403    if len(similar) == 1:
404        return _(b"did you mean %s?") % similar[0]
405    elif similar:
406        ss = b", ".join(sorted(similar))
407        return _(b"did you mean one of %s?") % ss
408    else:
409        return None
410
411
412class UnknownIdentifier(ParseError):
413    """Exception raised when a {rev,file}set references an unknown identifier"""
414
415    def __init__(self, function, symbols):
416        # type: (bytes, Iterable[bytes]) -> None
417        from .i18n import _
418
419        similar = getsimilar(symbols, function)
420        hint = similarity_hint(similar)
421
422        ParseError.__init__(
423            self, _(b"unknown identifier: %s") % function, hint=hint
424        )
425
426
427class RepoError(Hint, Exception):
428    __bytes__ = _tobytes
429
430
431class RepoLookupError(RepoError):
432    pass
433
434
435class FilteredRepoLookupError(RepoLookupError):
436    pass
437
438
439class CapabilityError(RepoError):
440    pass
441
442
443class RequirementError(RepoError):
444    """Exception raised if .hg/requires has an unknown entry."""
445
446
447class StdioError(IOError):
448    """Raised if I/O to stdout or stderr fails"""
449
450    def __init__(self, err):
451        # type: (IOError) -> None
452        IOError.__init__(self, err.errno, err.strerror)
453
454    # no __bytes__() because error message is derived from the standard IOError
455
456
457class UnsupportedMergeRecords(Abort):
458    def __init__(self, recordtypes):
459        # type: (Iterable[bytes]) -> None
460        from .i18n import _
461
462        self.recordtypes = sorted(recordtypes)
463        s = b' '.join(self.recordtypes)
464        Abort.__init__(
465            self,
466            _(b'unsupported merge state records: %s') % s,
467            hint=_(
468                b'see https://mercurial-scm.org/wiki/MergeStateRecords for '
469                b'more information'
470            ),
471        )
472
473
474class UnknownVersion(Abort):
475    """generic exception for aborting from an encounter with an unknown version"""
476
477    def __init__(self, msg, hint=None, version=None):
478        # type: (bytes, Optional[bytes], Optional[bytes]) -> None
479        self.version = version
480        super(UnknownVersion, self).__init__(msg, hint=hint)
481
482
483class LockError(IOError):
484    def __init__(self, errno, strerror, filename, desc):
485        # TODO: figure out if this should be bytes or str
486        # _type: (int, str, str, bytes) -> None
487        IOError.__init__(self, errno, strerror, filename)
488        self.desc = desc
489
490    # no __bytes__() because error message is derived from the standard IOError
491
492
493class LockHeld(LockError):
494    def __init__(self, errno, filename, desc, locker):
495        LockError.__init__(self, errno, b'Lock held', filename, desc)
496        self.locker = locker
497
498
499class LockUnavailable(LockError):
500    pass
501
502
503# LockError is for errors while acquiring the lock -- this is unrelated
504class LockInheritanceContractViolation(RuntimeError):
505    __bytes__ = _tobytes
506
507
508class ResponseError(Exception):
509    """Raised to print an error with part of output and exit."""
510
511    __bytes__ = _tobytes
512
513
514# derived from KeyboardInterrupt to simplify some breakout code
515class SignalInterrupt(KeyboardInterrupt):
516    """Exception raised on SIGTERM and SIGHUP."""
517
518
519class SignatureError(Exception):
520    __bytes__ = _tobytes
521
522
523class PushRaced(RuntimeError):
524    """An exception raised during unbundling that indicate a push race"""
525
526    __bytes__ = _tobytes
527
528
529class ProgrammingError(Hint, RuntimeError):
530    """Raised if a mercurial (core or extension) developer made a mistake"""
531
532    def __init__(self, msg, *args, **kwargs):
533        # type: (AnyStr, Any, Any) -> None
534        # On Python 3, turn the message back into a string since this is
535        # an internal-only error that won't be printed except in a
536        # stack traces.
537        msg = pycompat.sysstr(msg)
538        super(ProgrammingError, self).__init__(msg, *args, **kwargs)
539
540    __bytes__ = _tobytes
541
542
543class WdirUnsupported(Exception):
544    """An exception which is raised when 'wdir()' is not supported"""
545
546    __bytes__ = _tobytes
547
548
549# bundle2 related errors
550class BundleValueError(ValueError):
551    """error raised when bundle2 cannot be processed"""
552
553    __bytes__ = _tobytes
554
555
556class BundleUnknownFeatureError(BundleValueError):
557    def __init__(self, parttype=None, params=(), values=()):
558        self.parttype = parttype
559        self.params = params
560        self.values = values
561        if self.parttype is None:
562            msg = b'Stream Parameter'
563        else:
564            msg = parttype
565        entries = self.params
566        if self.params and self.values:
567            assert len(self.params) == len(self.values)
568            entries = []
569            for idx, par in enumerate(self.params):
570                val = self.values[idx]
571                if val is None:
572                    entries.append(val)
573                else:
574                    entries.append(b"%s=%r" % (par, pycompat.maybebytestr(val)))
575        if entries:
576            msg = b'%s - %s' % (msg, b', '.join(entries))
577        ValueError.__init__(self, msg)  # TODO: convert to str?
578
579
580class ReadOnlyPartError(RuntimeError):
581    """error raised when code tries to alter a part being generated"""
582
583    __bytes__ = _tobytes
584
585
586class PushkeyFailed(Abort):
587    """error raised when a pushkey part failed to update a value"""
588
589    def __init__(
590        self, partid, namespace=None, key=None, new=None, old=None, ret=None
591    ):
592        self.partid = partid
593        self.namespace = namespace
594        self.key = key
595        self.new = new
596        self.old = old
597        self.ret = ret
598        # no i18n expected to be processed into a better message
599        Abort.__init__(
600            self, b'failed to update value for "%s/%s"' % (namespace, key)
601        )
602
603
604class CensoredNodeError(StorageError):
605    """error raised when content verification fails on a censored node
606
607    Also contains the tombstone data substituted for the uncensored data.
608    """
609
610    def __init__(self, filename, node, tombstone):
611        # type: (bytes, bytes, bytes) -> None
612        from .node import short
613
614        StorageError.__init__(self, b'%s:%s' % (filename, short(node)))
615        self.tombstone = tombstone
616
617
618class CensoredBaseError(StorageError):
619    """error raised when a delta is rejected because its base is censored
620
621    A delta based on a censored revision must be formed as single patch
622    operation which replaces the entire base with new content. This ensures
623    the delta may be applied by clones which have not censored the base.
624    """
625
626
627class InvalidBundleSpecification(Exception):
628    """error raised when a bundle specification is invalid.
629
630    This is used for syntax errors as opposed to support errors.
631    """
632
633    __bytes__ = _tobytes
634
635
636class UnsupportedBundleSpecification(Exception):
637    """error raised when a bundle specification is not supported."""
638
639    __bytes__ = _tobytes
640
641
642class CorruptedState(Exception):
643    """error raised when a command is not able to read its state from file"""
644
645    __bytes__ = _tobytes
646
647
648class PeerTransportError(Abort):
649    """Transport-level I/O error when communicating with a peer repo."""
650
651
652class InMemoryMergeConflictsError(Exception):
653    """Exception raised when merge conflicts arose during an in-memory merge."""
654
655    __bytes__ = _tobytes
656
657
658class WireprotoCommandError(Exception):
659    """Represents an error during execution of a wire protocol command.
660
661    Should only be thrown by wire protocol version 2 commands.
662
663    The error is a formatter string and an optional iterable of arguments.
664    """
665
666    def __init__(self, message, args=None):
667        # type: (bytes, Optional[Sequence[bytes]]) -> None
668        self.message = message
669        self.messageargs = args
670