Patchwork D9167: errors: introduce UserError and use it from commands and cmdutil

login
register
mail settings
Submitter phabricator
Date Oct. 7, 2020, 6:12 a.m.
Message ID <differential-rev-PHID-DREV-5mywjuuop6tnthww4vup-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/47396/
State New
Headers show

Comments

phabricator - Oct. 7, 2020, 6:12 a.m.
martinvonz created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  This patch introduces a `UserError` class and replaces many uses of
  `error.Abort` by it in `commands` and `cmdutil`. This is a part of
  https://www.mercurial-scm.org/wiki/ErrorCategoriesPlan. There will
  later be a different class for state errors (to raise e.g. when
  there's an unfinished operation). It's not always clear when one
  should report a user error and when it should be a state error. We can
  always adjust later if I got something wrong in this patch (but feel
  free to point out any you notice now).

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D9167

AFFECTED FILES
  hgext/largefiles/overrides.py
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/dispatch.py
  mercurial/error.py
  mercurial/scmutil.py
  mercurial/subrepo.py

CHANGE DETAILS




To: martinvonz, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -81,7 +81,7 @@ 
         except SubrepoAbort as ex:
             # This exception has already been handled
             raise ex
-        except error.Abort as ex:
+        except (error.Abort, error.UserError) as ex:
             subrepo = subrelpath(self)
             errormsg = (
                 stringutil.forcebytestr(ex)
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -219,6 +219,10 @@ 
         ui.error(_(b"abort: %s\n") % inst)
         if inst.hint:
             ui.error(_(b"(%s)\n") % inst.hint)
+    except error.UserError as inst:
+        ui.error(_(b"abort: %s\n") % inst.message)
+        if inst.hint:
+            ui.error(_(b"(%s)\n") % inst.hint)
     except ImportError as inst:
         ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
         m = stringutil.forcebytestr(inst).split()[-1]
diff --git a/mercurial/error.py b/mercurial/error.py
--- a/mercurial/error.py
+++ b/mercurial/error.py
@@ -38,6 +38,19 @@ 
         super(Hint, self).__init__(*args, **kw)
 
 
+class UserError(Hint, Exception):
+    """Indicates that the user made an error.
+
+    Examples: Invalid command, invalid flags, invalid revision.
+    """
+
+    def __init__(self, message, hint=None):
+        self.message = message
+        super(UserError, self).__init__(hint=hint)
+
+    __bytes__ = _tobytes
+
+
 class StorageError(Hint, Exception):
     """Raised when an error occurs in a storage layer.
 
diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -288,7 +288,12 @@ 
             if req.fmsg:
                 req.ui.fmsg = req.fmsg
         except error.Abort as inst:
-            ferr.write(_(b"abort: %s\n") % inst)
+            ferr.write(_(b"abort from dispatch: %s\n") % inst)
+            if inst.hint:
+                ferr.write(_(b"(%s)\n") % inst.hint)
+            return -1
+        except error.UserError as inst:
+            ferr.write(_(b"abort: %s\n") % inst.message)
             if inst.hint:
                 ferr.write(_(b"(%s)\n") % inst.hint)
             return -1
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2034,11 +2034,13 @@ 
         extra[b'close'] = b'1'
 
         if repo[b'.'].closesbranch():
-            raise error.Abort(
+            raise error.UserError(
                 _(b'current revision is already a branch closing head')
             )
         elif not bheads:
-            raise error.Abort(_(b'branch "%s" has no heads to close') % branch)
+            raise error.UserError(
+                _(b'branch "%s" has no heads to close') % branch
+            )
         elif (
             branch == repo[b'.'].branch()
             and repo[b'.'].node() not in bheads
@@ -2048,17 +2050,19 @@ 
                 b'use --force-close-branch to close branch from a non-head'
                 b' changeset'
             )
-            raise error.Abort(_(b'can only close branch heads'), hint=hint)
+            raise error.UserError(_(b'can only close branch heads'), hint=hint)
         elif opts.get(b'amend'):
             if (
                 repo[b'.'].p1().branch() != branch
                 and repo[b'.'].p2().branch() != branch
             ):
-                raise error.Abort(_(b'can only close branch heads'))
+                raise error.UserError(_(b'can only close branch heads'))
 
     if opts.get(b'amend'):
         if ui.configbool(b'ui', b'commitsubrepos'):
-            raise error.Abort(_(b'cannot amend with ui.commitsubrepos enabled'))
+            raise error.UserError(
+                _(b'cannot amend with ui.commitsubrepos enabled')
+            )
 
         old = repo[b'.']
         rewriteutil.precheck(repo, [old.rev()], b'amend')
@@ -2196,17 +2200,19 @@ 
         cmdutil.check_at_most_one_arg(opts, *editopts[1:])
         if opts.get(b'local'):
             if not repo:
-                raise error.Abort(_(b"can't use --local outside a repository"))
+                raise error.UserError(
+                    _(b"can't use --local outside a repository")
+                )
             paths = [repo.vfs.join(b'hgrc')]
         elif opts.get(b'global'):
             paths = rcutil.systemrcpath()
         elif opts.get(b'shared'):
             if not repo.shared():
-                raise error.Abort(
+                raise error.UserError(
                     _(b"repository is not shared; can't use --shared")
                 )
                 if requirements.SHARESAFE_REQUIREMENT not in repo.requirements:
-                    raise error.Abort(
+                    raise error.UserError(
                         _(
                             b"share safe feature not unabled; "
                             b"unable to edit shared source repository config"
@@ -2235,7 +2241,7 @@ 
         editor = ui.geteditor()
         ui.system(
             b"%s \"%s\"" % (editor, f),
-            onerr=error.Abort,
+            onerr=error.UserError,
             errprefix=_(b"edit failed"),
             blockedtag=b'config_edit',
         )
@@ -2643,7 +2649,7 @@ 
 
     if bookmark:
         if bookmark not in repo._bookmarks:
-            raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
+            raise error.UserError(_(b"bookmark '%s' not found") % bookmark)
 
         revs = scmutil.bookmarkrevs(repo, bookmark)
     else:
@@ -2654,7 +2660,7 @@ 
         revs = scmutil.revrange(repo, changesets)
 
     if not revs:
-        raise error.Abort(_(b"export requires at least one changeset"))
+        raise error.UserError(_(b"export requires at least one changeset"))
     if len(revs) > 1:
         ui.note(_(b'exporting patches:\n'))
     else:
@@ -2819,7 +2825,7 @@ 
 
     opts = pycompat.byteskwargs(opts)
     if not pats:
-        raise error.Abort(_(b'no files specified'))
+        raise error.UserError(_(b'no files specified'))
 
     m = scmutil.match(repo[None], pats, opts)
     dryrun, interactive = opts.get(b'dry_run'), opts.get(b'interactive')
@@ -3046,7 +3052,7 @@ 
     elif opts.get(b'continue'):
         cont = True
         if revs:
-            raise error.Abort(_(b"can't specify --continue and revisions"))
+            raise error.UserError(_(b"can't specify --continue and revisions"))
         # read in unfinished revisions
         if graftstate.exists():
             statedata = cmdutil.readgraftstate(repo, graftstate)
@@ -3066,7 +3072,7 @@ 
             cmdutil.wrongtooltocontinue(repo, _(b'graft'))
     else:
         if not revs:
-            raise error.Abort(_(b'no revisions specified'))
+            raise error.UserError(_(b'no revisions specified'))
         cmdutil.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
         revs = scmutil.revrange(repo, revs)
@@ -3084,7 +3090,7 @@ 
     if not revs:
         return -1
     if basectx is not None and len(revs) != 1:
-        raise error.Abort(_(b'only one revision allowed with --base '))
+        raise error.UserError(_(b'only one revision allowed with --base '))
 
     # Don't check in the --continue case, in effect retaining --force across
     # --continues. That's because without --force, any revisions we decided to
@@ -3372,7 +3378,9 @@ 
     opts = pycompat.byteskwargs(opts)
     diff = opts.get(b'all') or opts.get(b'diff')
     if diff and opts.get(b'all_files'):
-        raise error.Abort(_(b'--diff and --all-files are mutually exclusive'))
+        raise error.UserError(
+            _(b'--diff and --all-files are mutually exclusive')
+        )
     if opts.get(b'all_files') is None and not diff:
         opts[b'all_files'] = True
     plaingrep = (
@@ -3916,7 +3924,7 @@ 
 
     opts = pycompat.byteskwargs(opts)
     if not repo and not source:
-        raise error.Abort(
+        raise error.UserError(
             _(b"there is no Mercurial repository here (.hg not found)")
         )
 
@@ -3935,7 +3943,7 @@ 
 
     if not repo:
         if num or branch or tags:
-            raise error.Abort(
+            raise error.UserError(
                 _(b"can't query remote revision number, branch, or tags")
             )
         if not rev and revs:
@@ -4203,7 +4211,7 @@ 
 
     opts = pycompat.byteskwargs(opts)
     if not patch1:
-        raise error.Abort(_(b'need at least one patch to import'))
+        raise error.UserError(_(b'need at least one patch to import'))
 
     patches = (patch1,) + patches
 
@@ -4214,22 +4222,22 @@ 
     exact = opts.get(b'exact')
     update = not opts.get(b'bypass')
     if not update and opts.get(b'no_commit'):
-        raise error.Abort(_(b'cannot use --no-commit with --bypass'))
+        raise error.UserError(_(b'cannot use --no-commit with --bypass'))
     if opts.get(b'secret') and opts.get(b'no_commit'):
-        raise error.Abort(_(b'cannot use --no-commit with --secret'))
+        raise error.UserError(_(b'cannot use --no-commit with --secret'))
     try:
         sim = float(opts.get(b'similarity') or 0)
     except ValueError:
-        raise error.Abort(_(b'similarity must be a number'))
+        raise error.UserError(_(b'similarity must be a number'))
     if sim < 0 or sim > 100:
-        raise error.Abort(_(b'similarity must be between 0 and 100'))
+        raise error.UserError(_(b'similarity must be between 0 and 100'))
     if sim and not update:
-        raise error.Abort(_(b'cannot use --similarity with --bypass'))
+        raise error.UserError(_(b'cannot use --similarity with --bypass'))
     if exact:
         if opts.get(b'edit'):
-            raise error.Abort(_(b'cannot use --exact with --edit'))
+            raise error.UserError(_(b'cannot use --exact with --edit'))
         if opts.get(b'prefix'):
-            raise error.Abort(_(b'cannot use --exact with --prefix'))
+            raise error.UserError(_(b'cannot use --exact with --prefix'))
 
     base = opts[b"base"]
     msgs = []
@@ -4743,11 +4751,11 @@ 
     linerange = opts.get(b'line_range')
 
     if linerange and not opts.get(b'follow'):
-        raise error.Abort(_(b'--line-range requires --follow'))
+        raise error.UserError(_(b'--line-range requires --follow'))
 
     if linerange and pats:
         # TODO: take pats as patterns with no line-range filter
-        raise error.Abort(
+        raise error.UserError(
             _(b'FILE arguments are not compatible with --line-range option')
         )
 
@@ -4809,7 +4817,7 @@ 
 
     if opts.get(b'all'):
         if rev or node:
-            raise error.Abort(_(b"can't specify a revision with --all"))
+            raise error.UserError(_(b"can't specify a revision with --all"))
 
         res = set()
         for rev in repo:
@@ -4824,7 +4832,7 @@ 
         return
 
     if rev and node:
-        raise error.Abort(_(b"please specify just one revision"))
+        raise error.UserError(_(b"please specify just one revision"))
 
     if not node:
         node = rev
@@ -4911,11 +4919,11 @@ 
                 hint=state.hint(),
             )
         if node:
-            raise error.Abort(_(b"cannot specify a node with --abort"))
+            raise error.UserError(_(b"cannot specify a node with --abort"))
         return hg.abortmerge(repo.ui, repo)
 
     if opts.get(b'rev') and node:
-        raise error.Abort(_(b"please specify just one revision"))
+        raise error.UserError(_(b"please specify just one revision"))
     if not node:
         node = opts.get(b'rev')
 
@@ -4923,7 +4931,7 @@ 
         ctx = scmutil.revsingle(repo, node)
     else:
         if ui.configbool(b'commands', b'merge.require-rev'):
-            raise error.Abort(
+            raise error.UserError(
                 _(
                     b'configuration requires specifying revision to merge '
                     b'with'
@@ -4932,7 +4940,7 @@ 
         ctx = repo[destutil.destmerge(repo)]
 
     if ctx.node() is None:
-        raise error.Abort(_(b'merging with the working copy has no effect'))
+        raise error.UserError(_(b'merging with the working copy has no effect'))
 
     if opts.get(b'preview'):
         # find nodes that are ancestors of p2 but not of p1
@@ -5125,7 +5133,7 @@ 
     if file_:
         m = scmutil.match(ctx, (file_,), opts)
         if m.anypats() or len(m.files()) != 1:
-            raise error.Abort(_(b'can only specify an explicit filename'))
+            raise error.UserError(_(b'can only specify an explicit filename'))
         file_ = m.files()[0]
         filenodes = []
         for cp in ctx.parents():
@@ -5136,7 +5144,7 @@ 
             except error.LookupError:
                 pass
         if not filenodes:
-            raise error.Abort(_(b"'%s' not found in manifest!") % file_)
+            raise error.UserError(_(b"'%s' not found in manifest!") % file_)
         p = []
         for fn in filenodes:
             fctx = repo.filectx(file_, fileid=fn)
@@ -5278,7 +5286,7 @@ 
     for idx, name in enumerate(phases.cmdphasenames):
         if opts[name]:
             if targetphase is not None:
-                raise error.Abort(_(b'only one phase can be specified'))
+                raise error.UserError(_(b'only one phase can be specified'))
             targetphase = idx
 
     # look for specified revision
@@ -5301,7 +5309,7 @@ 
         with repo.lock(), repo.transaction(b"phase") as tr:
             # set phase
             if not revs:
-                raise error.Abort(_(b'empty revision set'))
+                raise error.UserError(_(b'empty revision set'))
             nodes = [repo[r].node() for r in revs]
             # moving revision from public to draft may hide them
             # We have to check result on an unfiltered repository
@@ -5444,7 +5452,7 @@ 
     ):
         msg = _(b'update destination required by configuration')
         hint = _(b'use hg pull followed by hg update DEST')
-        raise error.Abort(msg, hint=hint)
+        raise error.UserError(msg, hint=hint)
 
     source, branches = hg.parseurl(ui.expandpath(source), opts.get(b'branch'))
     ui.status(_(b'pulling from %s\n') % util.hidepassword(source))
@@ -5483,7 +5491,9 @@ 
             for b in opts.get(b'bookmark', []):
                 b = repo._bookmarks.expandname(b)
                 if b not in remotebookmarks:
-                    raise error.Abort(_(b'remote bookmark %s not found!') % b)
+                    raise error.UserError(
+                        _(b'remote bookmark %s not found!') % b
+                    )
                 nodes.append(remotebookmarks[b])
             for i, rev in enumerate(revs):
                 node = fnodes[i].result()
@@ -5663,7 +5673,7 @@ 
     if revs:
         revs = [repo[r].node() for r in scmutil.revrange(repo, revs)]
         if not revs:
-            raise error.Abort(
+            raise error.UserError(
                 _(b"specified revisions evaluate to an empty set"),
                 hint=_(b"use different revision arguments"),
             )
@@ -5674,11 +5684,11 @@ 
         revs = scmutil.revrange(repo, [expr])
         revs = [repo[rev].node() for rev in revs]
         if not revs:
-            raise error.Abort(
+            raise error.UserError(
                 _(b'default push revset for path evaluates to an empty set')
             )
     elif ui.configbool(b'commands', b'push.require-revs'):
-        raise error.Abort(
+        raise error.UserError(
             _(b'no revisions specified to push'),
             hint=_(b'did you mean "hg push -r ."?'),
         )
@@ -5807,7 +5817,7 @@ 
     after, force = opts.get(b'after'), opts.get(b'force')
     dryrun = opts.get(b'dry_run')
     if not pats and not after:
-        raise error.Abort(_(b'no files specified'))
+        raise error.UserError(_(b'no files specified'))
 
     m = scmutil.match(repo[None], pats, opts)
     subrepos = opts.get(b'subrepos')
@@ -5937,16 +5947,16 @@ 
 
     actioncount = len(list(filter(None, [show, mark, unmark, remerge])))
     if actioncount > 1:
-        raise error.Abort(_(b"too many actions specified"))
+        raise error.UserError(_(b"too many actions specified"))
     elif actioncount == 0 and ui.configbool(
         b'commands', b'resolve.explicit-re-merge'
     ):
         hint = _(b'use --mark, --unmark, --list or --re-merge')
-        raise error.Abort(_(b'no action specified'), hint=hint)
+        raise error.UserError(_(b'no action specified'), hint=hint)
     if pats and all:
-        raise error.Abort(_(b"can't specify --all and patterns"))
+        raise error.UserError(_(b"can't specify --all and patterns"))
     if not (all or pats or show or mark or unmark):
-        raise error.Abort(
+        raise error.UserError(
             _(b'no files or directories specified'),
             hint=b'use --all to re-merge all unresolved files',
         )
@@ -5956,7 +5966,7 @@ 
             if ui.promptchoice(
                 _(b're-merge all unresolved files (yn)?$$ &Yes $$ &No')
             ):
-                raise error.Abort(_(b'user quit'))
+                raise error.UserError(_(b'user quit'))
         if mark and not pats:
             if ui.promptchoice(
                 _(
@@ -5964,7 +5974,7 @@ 
                     b'$$ &Yes $$ &No'
                 )
             ):
-                raise error.Abort(_(b'user quit'))
+                raise error.UserError(_(b'user quit'))
         if unmark and not pats:
             if ui.promptchoice(
                 _(
@@ -5972,7 +5982,7 @@ 
                     b'$$ &Yes $$ &No'
                 )
             ):
-                raise error.Abort(_(b'user quit'))
+                raise error.UserError(_(b'user quit'))
 
     uipathfn = scmutil.getuipathfn(repo)
 
@@ -6225,13 +6235,13 @@ 
     opts = pycompat.byteskwargs(opts)
     if opts.get(b"date"):
         if opts.get(b"rev"):
-            raise error.Abort(_(b"you can't specify a revision and a date"))
+            raise error.UserError(_(b"you can't specify a revision and a date"))
         opts[b"rev"] = cmdutil.finddate(ui, repo, opts[b"date"])
 
     parent, p2 = repo.dirstate.parents()
     if not opts.get(b'rev') and p2 != nullid:
         # revert after merge is a trap for new users (issue2915)
-        raise error.Abort(
+        raise error.UserError(
             _(b'uncommitted merge with no revision specified'),
             hint=_(b"use 'hg update' or see 'hg help revert'"),
         )
@@ -6254,7 +6264,7 @@ 
                 b"uncommitted merge, use --all to discard all changes,"
                 b" or 'hg update -C .' to abort the merge"
             )
-            raise error.Abort(msg, hint=hint)
+            raise error.UserError(msg, hint=hint)
         dirty = any(repo.status())
         node = ctx.node()
         if node != parent:
@@ -6278,7 +6288,7 @@ 
             hint = _(b"uncommitted changes, use --all to discard all changes")
         else:
             hint = _(b"use --all to revert all files")
-        raise error.Abort(msg, hint=hint)
+        raise error.UserError(msg, hint=hint)
 
     return cmdutil.revert(ui, repo, ctx, *pats, **pycompat.strkwargs(opts))
 
@@ -6487,9 +6497,9 @@ 
 
     opts = pycompat.byteskwargs(opts)
     if opts[b"stdio"] and opts[b"cmdserver"]:
-        raise error.Abort(_(b"cannot use --stdio with --cmdserver"))
+        raise error.UserError(_(b"cannot use --stdio with --cmdserver"))
     if opts[b"print_url"] and ui.verbose:
-        raise error.Abort(_(b"cannot use --print-url with --verbose"))
+        raise error.UserError(_(b"cannot use --print-url with --verbose"))
 
     if opts[b"stdio"]:
         if repo is None:
@@ -6615,7 +6625,7 @@ 
         if opts.get(opt):
             for i, allowable in allowables:
                 if opts[i] and opt not in allowable:
-                    raise error.Abort(
+                    raise error.UserError(
                         _(
                             b"options '--%s' and '--%s' may not be "
                             b"used together"
@@ -6626,7 +6636,9 @@ 
 
     if checkopt(b'cleanup'):
         if pats:
-            raise error.Abort(_(b"cannot specify names when using '--cleanup'"))
+            raise error.UserError(
+                _(b"cannot specify names when using '--cleanup'")
+            )
         return shelvemod.cleanupcmd(ui, repo)
     elif checkopt(b'delete'):
         return shelvemod.deletecmd(ui, repo, pats)
@@ -6792,7 +6804,7 @@ 
 
     if revs and terse:
         msg = _(b'cannot use --terse with --rev')
-        raise error.Abort(msg)
+        raise error.UserError(msg)
     elif change:
         repo = scmutil.unhidehashlikerevs(repo, [change], b'nowarn')
         ctx2 = scmutil.revsingle(repo, change, None)
@@ -7262,15 +7274,15 @@ 
         rev_ = b"."
         names = [t.strip() for t in (name1,) + names]
         if len(names) != len(set(names)):
-            raise error.Abort(_(b'tag names must be unique'))
+            raise error.UserError(_(b'tag names must be unique'))
         for n in names:
             scmutil.checknewlabel(repo, n, b'tag')
             if not n:
-                raise error.Abort(
+                raise error.UserError(
                     _(b'tag names cannot consist entirely of whitespace')
                 )
         if opts.get(b'rev') and opts.get(b'remove'):
-            raise error.Abort(_(b"--rev and --remove are incompatible"))
+            raise error.UserError(_(b"--rev and --remove are incompatible"))
         if opts.get(b'rev'):
             rev_ = opts[b'rev']
         message = opts.get(b'message')
@@ -7284,16 +7296,20 @@ 
                 if repo.tagtype(n) == b'global':
                     alltags = tagsmod.findglobaltags(ui, repo)
                     if alltags[n][0] == nullid:
-                        raise error.Abort(_(b"tag '%s' is already removed") % n)
+                        raise error.UserError(
+                            _(b"tag '%s' is already removed") % n
+                        )
                 if not repo.tagtype(n):
-                    raise error.Abort(_(b"tag '%s' does not exist") % n)
+                    raise error.UserError(_(b"tag '%s' does not exist") % n)
                 if repo.tagtype(n) != expectedtype:
                     if expectedtype == b'global':
-                        raise error.Abort(
+                        raise error.UserError(
                             _(b"tag '%s' is not a global tag") % n
                         )
                     else:
-                        raise error.Abort(_(b"tag '%s' is not a local tag") % n)
+                        raise error.UserError(
+                            _(b"tag '%s' is not a local tag") % n
+                        )
             rev_ = b'null'
             if not message:
                 # we don't translate commit messages
@@ -7301,7 +7317,7 @@ 
         elif not opts.get(b'force'):
             for n in names:
                 if n in repo.tags():
-                    raise error.Abort(
+                    raise error.UserError(
                         _(b"tag '%s' already exists (use -f to force)") % n
                     )
         if not opts.get(b'local'):
@@ -7342,7 +7358,7 @@ 
             not opts.get(b'remove')
             and scmutil.revsingle(repo, rev_).rev() == nullrev
         ):
-            raise error.Abort(_(b"cannot tag null revision"))
+            raise error.UserError(_(b"cannot tag null revision"))
 
         tagsmod.tag(
             repo,
@@ -7474,7 +7490,7 @@ 
             f = hg.openpath(ui, fname)
             gen = exchange.readbundle(ui, f, fname)
             if isinstance(gen, streamclone.streamcloneapplier):
-                raise error.Abort(
+                raise error.UserError(
                     _(
                         b'packed bundles cannot be applied with '
                         b'"hg unbundle"'
@@ -7669,11 +7685,11 @@ 
     check = opts.get('check')
     merge = opts.get('merge')
     if rev and node:
-        raise error.Abort(_(b"please specify just one revision"))
+        raise error.UserError(_(b"please specify just one revision"))
 
     if ui.configbool(b'commands', b'update.requiredest'):
         if not node and not rev and not date:
-            raise error.Abort(
+            raise error.UserError(
                 _(b'you must specify a destination'),
                 hint=_(b'for example: hg update ".::"'),
             )
@@ -7682,7 +7698,7 @@ 
         rev = node
 
     if date and rev is not None:
-        raise error.Abort(_(b"you can't specify a revision and a date"))
+        raise error.UserError(_(b"you can't specify a revision and a date"))
 
     updatecheck = None
     if check:
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -279,7 +279,7 @@ 
     for x in args:
         if opts.get(x):
             if previous:
-                raise error.Abort(
+                raise error.UserError(
                     _(b'cannot specify both --%s and --%s')
                     % (to_display(previous), to_display(x))
                 )
@@ -332,9 +332,9 @@ 
         return
 
     if len(note) > 255:
-        raise error.Abort(_(b"cannot store a note of more than 255 bytes"))
+        raise error.UserError(_(b"cannot store a note of more than 255 bytes"))
     if b'\n' in note:
-        raise error.Abort(_(b"note cannot contain a newline"))
+        raise error.UserError(_(b"note cannot contain a newline"))
 
 
 def ishunk(x):
@@ -426,7 +426,7 @@ 
             msg = _(b'running non-interactively, use %s instead') % cmdsuggest
         else:
             msg = _(b'running non-interactively')
-        raise error.Abort(msg)
+        raise error.UserError(msg)
 
     # make sure username is set before going interactive
     if not opts.get(b'user'):
@@ -451,7 +451,7 @@ 
         wctx = repo[None]
         merge = len(wctx.parents()) > 1
         if merge:
-            raise error.Abort(
+            raise error.UserError(
                 _(
                     b'cannot partially commit a merge '
                     b'(use "hg commit" instead)'
@@ -762,7 +762,7 @@ 
     # checking the argument validity
     for s in pycompat.bytestr(terseargs):
         if s not in allst:
-            raise error.Abort(_(b"'%s' not recognized") % s)
+            raise error.UserError(_(b"'%s' not recognized") % s)
 
     # creating a dirnode object for the root of the repo
     rootobj = dirnode(b'')
@@ -968,10 +968,10 @@ 
         bailifchanged(repo)
         revs = scmutil.revrange(repo, revs)
         if not revs:
-            raise error.Abort(b"empty revision set")
+            raise error.UserError(b"empty revision set")
         roots = repo.revs(b'roots(%ld)', revs)
         if len(roots) > 1:
-            raise error.Abort(
+            raise error.UserError(
                 _(b"cannot change branch of non-linear revisions")
             )
         rewriteutil.precheck(repo, revs, b'change branch of')
@@ -983,16 +983,20 @@ 
             and label not in rpb
             and label in repo.branchmap()
         ):
-            raise error.Abort(_(b"a branch of the same name already exists"))
+            raise error.UserError(
+                _(b"a branch of the same name already exists")
+            )
 
         if repo.revs(b'obsolete() and %ld', revs):
-            raise error.Abort(
+            raise error.UserError(
                 _(b"cannot change branch of a obsolete changeset")
             )
 
         # make sure only topological heads
         if repo.revs(b'heads(%ld) - head()', revs):
-            raise error.Abort(_(b"cannot change branch in middle of a stack"))
+            raise error.UserError(
+                _(b"cannot change branch in middle of a stack")
+            )
 
         replacements = {}
         # avoid import cycle mercurial.cmdutil -> mercurial.context ->
@@ -1349,7 +1353,7 @@ 
                 b'without a repository'
             )
     if msg:
-        raise error.Abort(msg)
+        raise error.UserError(msg)
 
     r = None
     if repo:
@@ -1357,7 +1361,7 @@ 
             r = repo.unfiltered().changelog
         elif dir:
             if not scmutil.istreemanifest(repo):
-                raise error.Abort(
+                raise error.UserError(
                     _(
                         b"--dir can only be used on repos with "
                         b"treemanifest enabled"
@@ -1383,11 +1387,13 @@ 
             elif util.safehasattr(r, b'_revlog'):
                 r = r._revlog  # pytype: disable=attribute-error
             elif r is not None:
-                raise error.Abort(_(b'%r does not appear to be a revlog') % r)
+                raise error.UserError(
+                    _(b'%r does not appear to be a revlog') % r
+                )
 
     if not r:
         if not returnrevlog:
-            raise error.Abort(_(b'cannot give path to non-revlog'))
+            raise error.UserError(_(b'cannot give path to non-revlog'))
 
         if not file_:
             raise error.CommandError(cmd, _(b'invalid arguments'))
@@ -1429,10 +1435,10 @@ 
         if not forget and not after:
             # TODO: Remove this restriction and make it also create the copy
             #       targets (and remove the rename source if rename==True).
-            raise error.Abort(_(b'--at-rev requires --after'))
+            raise error.UserError(_(b'--at-rev requires --after'))
         ctx = scmutil.revsingle(repo, rev)
         if len(ctx.parents()) > 1:
-            raise error.Abort(_(b'cannot mark/unmark copy in merge commit'))
+            raise error.UserError(_(b'cannot mark/unmark copy in merge commit'))
     else:
         ctx = repo[None]
 
@@ -1445,7 +1451,7 @@ 
             new_ctx = ctx
         else:
             if len(ctx.parents()) > 1:
-                raise error.Abort(_(b'cannot unmark copy in merge commit'))
+                raise error.UserError(_(b'cannot unmark copy in merge commit'))
             # avoid cycle context -> subrepo -> cmdutil
             from . import context
 
@@ -1488,9 +1494,9 @@ 
 
     pats = scmutil.expandpats(pats)
     if not pats:
-        raise error.Abort(_(b'no source or destination specified'))
+        raise error.UserError(_(b'no source or destination specified'))
     if len(pats) == 1:
-        raise error.Abort(_(b'no destination specified'))
+        raise error.UserError(_(b'no destination specified'))
     dest = pats.pop()
 
     def walkpat(pat):
@@ -1530,12 +1536,12 @@ 
         rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
         absdest = pathutil.canonpath(repo.root, cwd, dest)
         if ctx.hasdir(absdest):
-            raise error.Abort(
+            raise error.UserError(
                 _(b'%s: --at-rev does not support a directory as destination')
                 % uipathfn(absdest)
             )
         if absdest not in ctx:
-            raise error.Abort(
+            raise error.UserError(
                 _(b'%s: copy destination does not exist in %s')
                 % (uipathfn(absdest), ctx)
             )
@@ -1552,12 +1558,12 @@ 
                 copylist.append(abs)
 
         if not copylist:
-            raise error.Abort(_(b'no files to copy'))
+            raise error.UserError(_(b'no files to copy'))
         # TODO: Add support for `hg cp --at-rev . foo bar dir` and
         # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
         # existing functions below.
         if len(copylist) != 1:
-            raise error.Abort(_(b'--at-rev requires a single source'))
+            raise error.UserError(_(b'--at-rev requires a single source'))
 
         new_ctx = context.overlayworkingctx(repo)
         new_ctx.setbase(ctx.p1())
@@ -1785,14 +1791,16 @@ 
     destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
     if not destdirexists:
         if len(pats) > 1 or matchmod.patkind(pats[0]):
-            raise error.Abort(
+            raise error.UserError(
                 _(
                     b'with multiple sources, destination must be an '
                     b'existing directory'
                 )
             )
         if util.endswithsep(dest):
-            raise error.Abort(_(b'destination %s is not a directory') % dest)
+            raise error.UserError(
+                _(b'destination %s is not a directory') % dest
+            )
 
     tfn = targetpathfn
     if after:
@@ -1804,7 +1812,7 @@ 
             continue
         copylist.append((tfn(pat, dest, srcs), srcs))
     if not copylist:
-        raise error.Abort(_(b'no files to copy'))
+        raise error.UserError(_(b'no files to copy'))
 
     errors = 0
     for targetpath, srcs in copylist:
@@ -1895,7 +1903,7 @@ 
         parents.append(repo[nullid])
     if opts.get(b'exact'):
         if not nodeid or not p1:
-            raise error.Abort(_(b'not a Mercurial patch'))
+            raise error.UserError(_(b'not a Mercurial patch'))
         p1 = repo[p1]
         p2 = repo[p2 or nullid]
     elif p2:
@@ -2231,7 +2239,7 @@ 
     try:
         rev = mrevs.max()
     except ValueError:
-        raise error.Abort(_(b"revision matching date not found"))
+        raise error.UserError(_(b"revision matching date not found"))
 
     ui.status(
         _(b"found revision %d from %s\n")
@@ -2363,7 +2371,9 @@ 
     ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
 ):
     if dryrun and interactive:
-        raise error.Abort(_(b"cannot specify both --dry-run and --interactive"))
+        raise error.UserError(
+            _(b"cannot specify both --dry-run and --interactive")
+        )
     bad = []
     badfn = lambda x, y: bad.append(x) or match.bad(x, y)
     wctx = repo[None]
@@ -3075,9 +3085,9 @@ 
     if finishdesc:
         text = finishdesc(text)
     if not text.strip():
-        raise error.Abort(_(b"empty commit message"))
+        raise error.UserError(_(b"empty commit message"))
     if unchangedmessagedetection and editortext == templatetext:
-        raise error.Abort(_(b"commit message unchanged"))
+        raise error.UserError(_(b"commit message unchanged"))
 
     return text
 
diff --git a/hgext/largefiles/overrides.py b/hgext/largefiles/overrides.py
--- a/hgext/largefiles/overrides.py
+++ b/hgext/largefiles/overrides.py
@@ -733,8 +733,8 @@ 
     with extensions.wrappedfunction(scmutil, b'match', normalfilesmatchfn):
         try:
             result = orig(ui, repo, pats, opts, rename)
-        except error.Abort as e:
-            if pycompat.bytestr(e) != _(b'no files to copy'):
+        except error.UserError as e:
+            if e.message != _(b'no files to copy'):
                 raise e
             else:
                 nonormalfiles = True
@@ -850,8 +850,8 @@ 
 
                 lfdirstate.add(destlfile)
         lfdirstate.write()
-    except error.Abort as e:
-        if pycompat.bytestr(e) != _(b'no files to copy'):
+    except error.UserError as e:
+        if e.message != _(b'no files to copy'):
             raise e
         else:
             nolfiles = True
@@ -859,7 +859,7 @@ 
         wlock.release()
 
     if nolfiles and nonormalfiles:
-        raise error.Abort(_(b'no files to copy'))
+        raise error.UserError(_(b'no files to copy'))
 
     return result