stringutil: bulk-replace call sites to point to new module
authorYuya Nishihara <yuya@tcha.org>
Thu, 22 Mar 2018 21:56:20 +0900
changeset 44469 f0b6fbea00cfb0997d96b6ec71bec4d2b1b3e96a
parent 44468 f99d64e8a4e4366320f8037b4f18043b6f4254f1
child 44470 7ae6e3529e37492a5b012298cd3a936ff1c49f6e
push id766
push usergszorc@mozilla.com
push dateSat, 24 Mar 2018 00:21:29 +0000
stringutil: bulk-replace call sites to point to new module This might conflict with other patches floating around, sorry.
hgext/bugzilla.py
hgext/convert/cvsps.py
hgext/convert/p4.py
hgext/convert/subversion.py
hgext/eol.py
hgext/extdiff.py
hgext/highlight/highlight.py
hgext/histedit.py
hgext/journal.py
hgext/keyword.py
hgext/largefiles/remotestore.py
hgext/lfs/wrapper.py
hgext/mq.py
hgext/narrow/narrowbundle2.py
hgext/notify.py
hgext/relink.py
hgext/shelve.py
hgext/transplant.py
hgext/win32text.py
mercurial/branchmap.py
mercurial/bundle2.py
mercurial/changegroup.py
mercurial/changelog.py
mercurial/cmdutil.py
mercurial/color.py
mercurial/commands.py
mercurial/context.py
mercurial/crecord.py
mercurial/dagparser.py
mercurial/debugcommands.py
mercurial/dispatch.py
mercurial/exchange.py
mercurial/extensions.py
mercurial/filemerge.py
mercurial/fileset.py
mercurial/hg.py
mercurial/hgweb/webcommands.py
mercurial/hgweb/webutil.py
mercurial/localrepo.py
mercurial/logcmdutil.py
mercurial/mail.py
mercurial/match.py
mercurial/minirst.py
mercurial/parser.py
mercurial/patch.py
mercurial/repair.py
mercurial/revlog.py
mercurial/revset.py
mercurial/revsetlang.py
mercurial/scmutil.py
mercurial/simplemerge.py
mercurial/sslutil.py
mercurial/subrepo.py
mercurial/subrepoutil.py
mercurial/tags.py
mercurial/templatefilters.py
mercurial/templatekw.py
mercurial/templater.py
mercurial/templateutil.py
mercurial/ui.py
mercurial/url.py
mercurial/util.py
mercurial/wireproto.py
mercurial/wireprotoframing.py
tests/test-simplemerge.py
--- a/hgext/bugzilla.py
+++ b/hgext/bugzilla.py
@@ -302,16 +302,19 @@ from mercurial.node import short
 from mercurial import (
     error,
     logcmdutil,
     mail,
     registrar,
     url,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 xmlrpclib = util.xmlrpclib
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
@@ -1094,17 +1097,18 @@ class bugzilla(object):
         t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
         self.ui.pushbuffer()
         t.show(ctx, changes=ctx.changeset(),
                bug=str(bugid),
                hgweb=self.ui.config('web', 'baseurl'),
                root=self.repo.root,
                webroot=webroot(self.repo.root))
         data = self.ui.popbuffer()
-        self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
+        self.bzdriver.updatebug(bugid, newstate, data,
+                                stringutil.email(ctx.user()))
 
     def notify(self, bugs, committer):
         '''ensure Bugzilla users are notified of bug change.'''
         self.bzdriver.notify(bugs, committer)
 
 def hook(ui, repo, hooktype, node=None, **kwargs):
     '''add comment to bugzilla for each changeset that refers to a
     bugzilla bug id. only add a comment once per bug, so same change
@@ -1114,11 +1118,11 @@ def hook(ui, repo, hooktype, node=None, 
                          hooktype)
     try:
         bz = bugzilla(ui, repo)
         ctx = repo[node]
         bugs = bz.find_bugs(ctx)
         if bugs:
             for bug in bugs:
                 bz.update(bug, bugs[bug], ctx)
-            bz.notify(bugs, util.email(ctx.user()))
+            bz.notify(bugs, stringutil.email(ctx.user()))
     except Exception as e:
         raise error.Abort(_('Bugzilla error: %s') % e)
--- a/hgext/convert/cvsps.py
+++ b/hgext/convert/cvsps.py
@@ -12,17 +12,20 @@ import re
 from mercurial.i18n import _
 from mercurial import (
     encoding,
     error,
     hook,
     pycompat,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 pickle = util.pickle
 
 class logentry(object):
     '''Class logentry has the following attributes:
         .author    - author name as CVS knows it
         .branch    - name of branch this revision is on
         .branches  - revision tuple of branches starting at this revision
@@ -447,17 +450,18 @@ def createlog(ui, directory=None, root="
                         branchpoints.add(branch)
             e.branchpoints = branchpoints
 
             log.append(e)
 
             rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
 
             if len(log) % 100 == 0:
-                ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
+                ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
+                          + '\n')
 
     log.sort(key=lambda x: (x.rcs, x.revision))
 
     # find parent revisions of individual files
     versions = {}
     for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
         rcs = e.rcs.replace('/Attic/', '/')
         if rcs in rcsmap:
@@ -603,17 +607,17 @@ def createchangeset(ui, log, fuzz=60, me
                           branch=e.branch, date=e.date,
                           entries=[], mergepoint=e.mergepoint,
                           branchpoints=e.branchpoints, commitid=e.commitid)
             changesets.append(c)
 
             files = set()
             if len(changesets) % 100 == 0:
                 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
-                ui.status(util.ellipsis(t, 80) + '\n')
+                ui.status(stringutil.ellipsis(t, 80) + '\n')
 
         c.entries.append(e)
         files.add(e.file)
         c.date = e.date       # changeset date is date of latest commit in it
 
     # Mark synthetic changesets
 
     for c in changesets:
--- a/hgext/convert/p4.py
+++ b/hgext/convert/p4.py
@@ -9,17 +9,20 @@ from __future__ import absolute_import
 import marshal
 import re
 
 from mercurial.i18n import _
 from mercurial import (
     error,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 from . import common
 
 def loaditer(f):
     "Yield the dictionary objects generated by p4"
     try:
         while True:
             d = marshal.load(f)
@@ -164,17 +167,17 @@ class p4_source(common.converter_source)
 
             descarr = c.desc.splitlines(True)
             if len(descarr) > 0:
                 shortdesc = descarr[0].rstrip('\r\n')
             else:
                 shortdesc = '**empty changelist description**'
 
             t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
-            ui.status(util.ellipsis(t, 80) + '\n')
+            ui.status(stringutil.ellipsis(t, 80) + '\n')
 
             files = []
             copies = {}
             copiedfiles = []
             i = 0
             while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
                 oldname = d["depotFile%d" % i]
                 filename = None
--- a/hgext/convert/subversion.py
+++ b/hgext/convert/subversion.py
@@ -11,17 +11,20 @@ import xml.dom.minidom
 from mercurial.i18n import _
 from mercurial import (
     encoding,
     error,
     pycompat,
     util,
     vfs as vfsmod,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 from . import common
 
 pickle = util.pickle
 stringio = util.stringio
 propertycache = util.propertycache
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -142,17 +145,17 @@ def get_log_child(fp, url, paths, start,
         svn.ra.get_log(t.ra, paths, start, end, limit,
                        discover_changed_paths,
                        strict_node_history,
                        receiver)
     except IOError:
         # Caller may interrupt the iteration
         pickle.dump(None, fp, protocol)
     except Exception as inst:
-        pickle.dump(util.forcebytestr(inst), fp, protocol)
+        pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
     else:
         pickle.dump(None, fp, protocol)
     fp.flush()
     # With large history, cleanup process goes crazy and suddenly
     # consumes *huge* amount of memory. The output file being closed,
     # there is no need for clean termination.
     os._exit(0)
 
@@ -1310,17 +1313,17 @@ class svn_sink(converter_sink, commandli
             self.setexec = []
 
         fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
         fp = os.fdopen(fd, r'wb')
         fp.write(util.tonativeeol(commit.desc))
         fp.close()
         try:
             output = self.run0('commit',
-                               username=util.shortuser(commit.author),
+                               username=stringutil.shortuser(commit.author),
                                file=messagefile,
                                encoding='utf-8')
             try:
                 rev = self.commit_re.search(output).group(1)
             except AttributeError:
                 if parents and not files:
                     return parents[0]
                 self.ui.warn(_('unexpected svn output:\n'))
--- a/hgext/eol.py
+++ b/hgext/eol.py
@@ -100,16 +100,19 @@ from mercurial import (
     config,
     error as errormod,
     extensions,
     match,
     pycompat,
     registrar,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
 
 configtable = {}
@@ -128,28 +131,28 @@ configitem('eol', 'only-consistent',
 # Matches a lone LF, i.e., one that is not part of CRLF.
 singlelf = re.compile('(^|[^\r])\n')
 
 def inconsistenteol(data):
     return '\r\n' in data and singlelf.search(data)
 
 def tolf(s, params, ui, **kwargs):
     """Filter to convert to LF EOLs."""
-    if util.binary(s):
+    if stringutil.binary(s):
         return s
     if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
         return s
     if (ui.configbool('eol', 'fix-trailing-newline')
         and s and s[-1] != '\n'):
         s = s + '\n'
     return util.tolf(s)
 
 def tocrlf(s, params, ui, **kwargs):
     """Filter to convert to CRLF EOLs."""
-    if util.binary(s):
+    if stringutil.binary(s):
         return s
     if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
         return s
     if (ui.configbool('eol', 'fix-trailing-newline')
         and s and s[-1] != '\n'):
         s = s + '\n'
     return util.tocrlf(s)
 
@@ -398,17 +401,17 @@ def reposetup(ui, repo):
         def commitctx(self, ctx, error=False):
             for f in sorted(ctx.added() + ctx.modified()):
                 if not self._eolmatch(f):
                     continue
                 fctx = ctx[f]
                 if fctx is None:
                     continue
                 data = fctx.data()
-                if util.binary(data):
+                if stringutil.binary(data):
                     # We should not abort here, since the user should
                     # be able to say "** = native" to automatically
                     # have all non-binary files taken care of.
                     continue
                 if inconsistenteol(data):
                     raise errormod.Abort(_("inconsistent newline style "
                                            "in %s\n") % f)
             return super(eolrepo, self).commitctx(ctx, error)
--- a/hgext/extdiff.py
+++ b/hgext/extdiff.py
@@ -77,16 +77,19 @@ from mercurial import (
     cmdutil,
     error,
     filemerge,
     pycompat,
     registrar,
     scmutil,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 
 configtable = {}
 configitem = registrar.configitem(configtable)
 
 configitem('extdiff', br'opts\..*',
@@ -362,18 +365,18 @@ class savedcmd(object):
     that revision is compared to the working directory, and, when no
     revisions are specified, the working directory files are compared
     to its parent.
     """
 
     def __init__(self, path, cmdline):
         # We can't pass non-ASCII through docstrings (and path is
         # in an unknown encoding anyway)
-        docpath = util.escapestr(path)
-        self.__doc__ %= {r'path': pycompat.sysstr(util.uirepr(docpath))}
+        docpath = stringutil.escapestr(path)
+        self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
         self._cmdline = cmdline
 
     def __call__(self, ui, repo, *pats, **opts):
         opts = pycompat.byteskwargs(opts)
         options = ' '.join(map(util.shellquote, opts['option']))
         if options:
             options = ' ' + options
         return dodiff(ui, repo, self._cmdline + options, pats, opts)
--- a/hgext/highlight/highlight.py
+++ b/hgext/highlight/highlight.py
@@ -10,17 +10,20 @@
 
 from __future__ import absolute_import
 
 from mercurial import demandimport
 demandimport.ignore.extend(['pkgutil', 'pkg_resources', '__main__'])
 
 from mercurial import (
     encoding,
-    util,
+)
+
+from mercurial.utils import (
+    stringutil,
 )
 
 with demandimport.deactivated():
     import pygments
     import pygments.formatters
     import pygments.lexers
     import pygments.plugin
     import pygments.util
@@ -42,17 +45,17 @@ def pygmentize(field, fctx, style, tmpl,
 
     # append a <link ...> to the syntax highlighting css
     old_header = tmpl.load('header')
     if SYNTAX_CSS not in old_header:
         new_header = old_header + SYNTAX_CSS
         tmpl.cache['header'] = new_header
 
     text = fctx.data()
-    if util.binary(text):
+    if stringutil.binary(text):
         return
 
     # str.splitlines() != unicode.splitlines() because "reasons"
     for c in "\x0c\x1c\x1d\x1e":
         if c in text:
             text = text.replace(c, '')
 
     # Pygments is best used with Unicode strings:
--- a/hgext/histedit.py
+++ b/hgext/histedit.py
@@ -204,16 +204,19 @@ from mercurial import (
     node,
     obsolete,
     pycompat,
     registrar,
     repair,
     scmutil,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 pickle = util.pickle
 release = lock.release
 cmdtable = {}
 command = registrar.command(cmdtable)
 
 configtable = {}
 configitem = registrar.configitem(configtable)
@@ -460,17 +463,17 @@ class histeditaction(object):
         """
         ctx = self.repo[self.node]
         summary = _getsummary(ctx)
         line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
         # trim to 75 columns by default so it's not stupidly wide in my editor
         # (the 5 more are left for verb)
         maxlen = self.repo.ui.configint('histedit', 'linelen')
         maxlen = max(maxlen, 22) # avoid truncating hash
-        return util.ellipsis(line, maxlen)
+        return stringutil.ellipsis(line, maxlen)
 
     def tostate(self):
         """Print an action in format used by histedit state files
            (the first line is a verb, the remainder is the second)
         """
         return "%s\n%s" % (self.verb, node.hex(self.node))
 
     def run(self):
--- a/hgext/journal.py
+++ b/hgext/journal.py
@@ -31,17 +31,20 @@ from mercurial import (
     localrepo,
     lock,
     logcmdutil,
     node,
     pycompat,
     registrar,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
@@ -371,19 +374,19 @@ class journalstorage(object):
         Both the namespace and the name are optional; if neither is given all
         entries in the journal are produced.
 
         Matching supports regular expressions by using the `re:` prefix
         (use `literal:` to match names or namespaces that start with `re:`)
 
         """
         if namespace is not None:
-            namespace = util.stringmatcher(namespace)[-1]
+            namespace = stringutil.stringmatcher(namespace)[-1]
         if name is not None:
-            name = util.stringmatcher(name)[-1]
+            name = stringutil.stringmatcher(name)[-1]
         for entry in self:
             if namespace is not None and not namespace(entry.namespace):
                 continue
             if name is not None and not name(entry.name):
                 continue
             yield entry
 
     def __iter__(self):
--- a/hgext/keyword.py
+++ b/hgext/keyword.py
@@ -106,17 +106,20 @@ from mercurial import (
     patch,
     pathutil,
     pycompat,
     registrar,
     scmutil,
     templatefilters,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
@@ -267,17 +270,18 @@ class kwtemplater(object):
         return subfunc(kwsub, data)
 
     def linkctx(self, path, fileid):
         '''Similar to filelog.linkrev, but returns a changectx.'''
         return self.repo.filectx(path, fileid=fileid).changectx()
 
     def expand(self, path, node, data):
         '''Returns data with keywords expanded.'''
-        if not self.restrict and self.match(path) and not util.binary(data):
+        if (not self.restrict and self.match(path)
+            and not stringutil.binary(data)):
             ctx = self.linkctx(path, node)
             return self.substitute(data, path, ctx, self.rekw.sub)
         return data
 
     def iskwfile(self, cand, ctx):
         '''Returns subset of candidates which are configured for keyword
         expansion but are not symbolic links.'''
         return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
@@ -299,17 +303,17 @@ class kwtemplater(object):
             msg = _('overwriting %s expanding keywords\n')
         else:
             msg = _('overwriting %s shrinking keywords\n')
         for f in candidates:
             if self.restrict:
                 data = self.repo.file(f).read(mf[f])
             else:
                 data = self.repo.wread(f)
-            if util.binary(data):
+            if stringutil.binary(data):
                 continue
             if expand:
                 parents = ctx.parents()
                 if lookup:
                     ctx = self.linkctx(f, mf[f])
                 elif self.restrict and len(parents) > 1:
                     # merge commit
                     # in case of conflict f is in modified state during
@@ -330,25 +334,25 @@ class kwtemplater(object):
                 fp.close()
                 if kwcmd:
                     self.repo.dirstate.normal(f)
                 elif self.postcommit:
                     self.repo.dirstate.normallookup(f)
 
     def shrink(self, fname, text):
         '''Returns text with all keyword substitutions removed.'''
-        if self.match(fname) and not util.binary(text):
+        if self.match(fname) and not stringutil.binary(text):
             return _shrinktext(text, self.rekwexp.sub)
         return text
 
     def shrinklines(self, fname, lines):
         '''Returns lines with keyword substitutions removed.'''
         if self.match(fname):
             text = ''.join(lines)
-            if not util.binary(text):
+            if not stringutil.binary(text):
                 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
         return lines
 
     def wread(self, fname, data):
         '''If in restricted mode returns data read from wdir with
         keyword substitutions removed.'''
         if self.restrict:
             return self.shrink(fname, data)
--- a/hgext/largefiles/remotestore.py
+++ b/hgext/largefiles/remotestore.py
@@ -9,16 +9,20 @@ from __future__ import absolute_import
 
 from mercurial.i18n import _
 
 from mercurial import (
     error,
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 from . import (
     basestore,
     lfutil,
     localstore,
 )
 
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -47,35 +51,35 @@ class remotestore(basestore.basestore):
     def sendfile(self, filename, hash):
         self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
         try:
             with lfutil.httpsendfile(self.ui, filename) as fd:
                 return self._put(hash, fd)
         except IOError as e:
             raise error.Abort(
                 _('remotestore: could not open file %s: %s')
-                % (filename, util.forcebytestr(e)))
+                % (filename, stringutil.forcebytestr(e)))
 
     def _getfile(self, tmpfile, filename, hash):
         try:
             chunks = self._get(hash)
         except urlerr.httperror as e:
             # 401s get converted to error.Aborts; everything else is fine being
             # turned into a StoreError
             raise basestore.StoreError(filename, hash, self.url,
-                                       util.forcebytestr(e))
+                                       stringutil.forcebytestr(e))
         except urlerr.urlerror as e:
             # This usually indicates a connection problem, so don't
             # keep trying with the other files... they will probably
             # all fail too.
             raise error.Abort('%s: %s' %
                              (util.hidepassword(self.url), e.reason))
         except IOError as e:
             raise basestore.StoreError(filename, hash, self.url,
-                                       util.forcebytestr(e))
+                                       stringutil.forcebytestr(e))
 
         return lfutil.copyandhash(chunks, tmpfile)
 
     def _hashesavailablelocally(self, hashes):
         existslocallymap = self._lstore.exists(hashes)
         localhashes = [hash for hash in hashes if existslocallymap[hash]]
         return localhashes
 
--- a/hgext/lfs/wrapper.py
+++ b/hgext/lfs/wrapper.py
@@ -14,16 +14,20 @@ from mercurial.node import bin, hex, nul
 
 from mercurial import (
     error,
     filelog,
     revlog,
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 from ..largefiles import lfutil
 
 from . import (
     blobstore,
     pointer,
 )
 
 def supportedoutgoingversions(orig, repo):
@@ -90,17 +94,17 @@ def writetostore(self, text):
 
     # replace contents with metadata
     longoid = 'sha256:%s' % oid
     metadata = pointer.gitlfspointer(oid=longoid, size='%d' % len(text))
 
     # by default, we expect the content to be binary. however, LFS could also
     # be used for non-binary content. add a special entry for non-binary data.
     # this will be used by filectx.isbinary().
-    if not util.binary(text):
+    if not stringutil.binary(text):
         # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
         metadata['x-is-binary'] = '0'
 
     # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
     if hgmeta is not None:
         for k, v in hgmeta.iteritems():
             metadata['x-hg-%s' % k] = v
 
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -93,17 +93,20 @@ from mercurial import (
     registrar,
     revsetlang,
     scmutil,
     smartset,
     subrepoutil,
     util,
     vfs as vfsmod,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 release = lockmod.release
 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -464,17 +467,17 @@ class queue(object):
         self.added = []
         self.seriespath = "series"
         self.statuspath = "status"
         self.guardspath = "guards"
         self.activeguards = None
         self.guardsdirty = False
         # Handle mq.git as a bool with extended values
         gitmode = ui.config('mq', 'git').lower()
-        boolmode = util.parsebool(gitmode)
+        boolmode = stringutil.parsebool(gitmode)
         if boolmode is not None:
             if boolmode:
                 gitmode = 'yes'
             else:
                 gitmode = 'no'
         self.gitmode = gitmode
         # deprecated config: mq.plain
         self.plainmode = ui.configbool('mq', 'plain')
@@ -719,17 +722,17 @@ class queue(object):
     def removeundo(self, repo):
         undo = repo.sjoin('undo')
         if not os.path.exists(undo):
             return
         try:
             os.unlink(undo)
         except OSError as inst:
             self.ui.warn(_('error removing undo: %s\n') %
-                         util.forcebytestr(inst))
+                         stringutil.forcebytestr(inst))
 
     def backup(self, repo, files, copy=False):
         # backup local changes in --force case
         for f in sorted(files):
             absf = repo.wjoin(f)
             if os.path.lexists(absf):
                 self.ui.note(_('saving current version of %s as %s\n') %
                              (f, scmutil.origpath(self.ui, repo, f)))
@@ -852,17 +855,17 @@ class queue(object):
         '''Apply patchfile  to the working directory.
         patchfile: name of patch file'''
         files = set()
         try:
             fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
                                   files=files, eolmode=None)
             return (True, list(files), fuzz)
         except Exception as inst:
-            self.ui.note(util.forcebytestr(inst) + '\n')
+            self.ui.note(stringutil.forcebytestr(inst) + '\n')
             if not self.ui.verbose:
                 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
             self.ui.traceback()
             return (False, list(files), False)
 
     def apply(self, repo, series, list=False, update_status=True,
               strict=False, patchdir=None, merge=None, all_files=None,
               tobackup=None, keepchanges=False):
@@ -1912,17 +1915,17 @@ class queue(object):
                 if ph.message:
                     msg = ph.message[0]
                 else:
                     msg = ''
 
                 if self.ui.formatted():
                     width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
                     if width > 0:
-                        msg = util.ellipsis(msg, width)
+                        msg = stringutil.ellipsis(msg, width)
                     else:
                         msg = ''
                 self.ui.write(patchname, label='qseries.' + state)
                 self.ui.write(': ')
                 self.ui.write(msg, label='qseries.message.' + state)
             else:
                 self.ui.write(patchname, label='qseries.' + state)
             self.ui.write('\n')
--- a/hgext/narrow/narrowbundle2.py
+++ b/hgext/narrow/narrowbundle2.py
@@ -24,16 +24,19 @@ from mercurial import (
     error,
     exchange,
     extensions,
     narrowspec,
     repair,
     util,
     wireproto,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 NARROWCAP = 'narrow'
 _NARROWACL_SECTION = 'narrowhgacl'
 _CHANGESPECPART = NARROWCAP + ':changespec'
 _SPECPART = NARROWCAP + ':spec'
 _SPECPART_INCLUDE = 'include'
 _SPECPART_EXCLUDE = 'exclude'
 _KILLNODESIGNAL = 'KILL'
@@ -444,17 +447,17 @@ def handlechangegroup_widen(op, inpart):
 
     # remove undo files
     for undovfs, undofile in repo.undofiles():
         try:
             undovfs.unlink(undofile)
         except OSError as e:
             if e.errno != errno.ENOENT:
                 ui.warn(_('error removing %s: %s\n') %
-                        (undovfs.join(undofile), util.forcebytestr(e)))
+                        (undovfs.join(undofile), stringutil.forcebytestr(e)))
 
     # Remove partial backup only if there were no exceptions
     vfs.unlink(chgrpfile)
 
 def setup():
     """Enable narrow repo support in bundle2-related extension points."""
     extensions.wrapfunction(bundle2, 'getrepocaps', getrepocaps_narrow)
 
--- a/hgext/notify.py
+++ b/hgext/notify.py
@@ -144,17 +144,20 @@ from mercurial.i18n import _
 from mercurial import (
     error,
     logcmdutil,
     mail,
     patch,
     registrar,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
 
 configtable = {}
@@ -272,17 +275,17 @@ class notifier(object):
                 break
             path = path[c + 1:]
             count -= 1
         return path
 
     def fixmail(self, addr):
         '''try to clean up email addresses.'''
 
-        addr = util.email(addr.strip())
+        addr = stringutil.email(addr.strip())
         if self.domain:
             a = addr.find('@localhost')
             if a != -1:
                 addr = addr[:a]
             if '@' not in addr:
                 return addr + '@' + self.domain
         return addr
 
@@ -367,17 +370,17 @@ class notifier(object):
         if not subject:
             if count > 1:
                 subject = _('%s: %d new changesets') % (self.root, count)
             else:
                 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
                 subject = '%s: %s' % (self.root, s)
         maxsubject = int(self.ui.config('notify', 'maxsubject'))
         if maxsubject:
-            subject = util.ellipsis(subject, maxsubject)
+            subject = stringutil.ellipsis(subject, maxsubject)
         msg['Subject'] = mail.headencode(self.ui, subject,
                                          self.charsets, self.test)
 
         # try to make message have proper sender
         if not sender:
             sender = self.ui.config('email', 'from') or self.ui.username()
         if '@' not in sender or '@localhost' in sender:
             sender = self.fixmail(sender)
@@ -394,17 +397,17 @@ class notifier(object):
         msgtext = msg.as_string()
         if self.test:
             self.ui.write(msgtext)
             if not msgtext.endswith('\n'):
                 self.ui.write('\n')
         else:
             self.ui.status(_('notify: sending %d subscribers %d changes\n') %
                            (len(subs), count))
-            mail.sendmail(self.ui, util.email(msg['From']),
+            mail.sendmail(self.ui, stringutil.email(msg['From']),
                           subs, msgtext, mbox=self.mbox)
 
     def diff(self, ctx, ref=None):
 
         maxdiff = int(self.ui.config('notify', 'maxdiff'))
         prev = ctx.p1().node()
         if ref:
             ref = ref.node()
--- a/hgext/relink.py
+++ b/hgext/relink.py
@@ -13,16 +13,19 @@ import stat
 
 from mercurial.i18n import _
 from mercurial import (
     error,
     hg,
     registrar,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
@@ -182,14 +185,14 @@ def do_relink(src, dst, files, ui):
             ui.debug('not linkable: %s\n' % f)
             continue
         try:
             relinkfile(source, tgt)
             ui.progress(_('relinking'), pos, f, _('files'), total)
             relinked += 1
             savedbytes += sz
         except OSError as inst:
-            ui.warn('%s: %s\n' % (tgt, util.forcebytestr(inst)))
+            ui.warn('%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
 
     ui.progress(_('relinking'), None)
 
     ui.status(_('relinked %d files (%s reclaimed)\n') %
               (relinked, util.bytecount(savedbytes)))
--- a/hgext/shelve.py
+++ b/hgext/shelve.py
@@ -51,17 +51,20 @@ from mercurial import (
     templatefilters,
     util,
     vfs as vfsmod,
 )
 
 from . import (
     rebase,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
@@ -472,17 +475,17 @@ def _docreatecmd(ui, repo, pats, opts):
                                     **pycompat.strkwargs(opts))
         if not node:
             _nothingtoshelvemessaging(ui, repo, pats, opts)
             return 1
 
         _shelvecreatedcommit(repo, node, name)
 
         if ui.formatted():
-            desc = util.ellipsis(desc, ui.termwidth())
+            desc = stringutil.ellipsis(desc, ui.termwidth())
         ui.status(_('shelved as %s\n') % name)
         hg.update(repo, parent.node())
         if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
             repo.dirstate.setbranch(origbranch)
 
         _finishshelve(repo)
     finally:
         _restoreactivebookmark(repo, activebookmark)
@@ -573,17 +576,17 @@ def listcmd(ui, repo, pats, opts):
         with open(name + '.' + patchextension, 'rb') as fp:
             while True:
                 line = fp.readline()
                 if not line:
                     break
                 if not line.startswith('#'):
                     desc = line.rstrip()
                     if ui.formatted():
-                        desc = util.ellipsis(desc, width - used)
+                        desc = stringutil.ellipsis(desc, width - used)
                     ui.write(desc)
                     break
             ui.write('\n')
             if not (opts['patch'] or opts['stat']):
                 continue
             difflines = fp.readlines()
             if opts['patch']:
                 for chunk, label in patch.difflabel(iter, difflines):
--- a/hgext/transplant.py
+++ b/hgext/transplant.py
@@ -33,16 +33,19 @@ from mercurial import (
     registrar,
     revlog,
     revset,
     scmutil,
     smartset,
     util,
     vfs as vfsmod,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 class TransplantError(error.Abort):
     pass
 
 cmdtable = {}
 command = registrar.command(cmdtable)
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -306,17 +309,17 @@ class transplanter(object):
                 files = list(files)
             except Exception as inst:
                 seriespath = os.path.join(self.path, 'series')
                 if os.path.exists(seriespath):
                     os.unlink(seriespath)
                 p1 = repo.dirstate.p1()
                 p2 = node
                 self.log(user, date, message, p1, p2, merge=merge)
-                self.ui.write(util.forcebytestr(inst) + '\n')
+                self.ui.write(stringutil.forcebytestr(inst) + '\n')
                 raise TransplantError(_('fix up the working directory and run '
                                         'hg transplant --continue'))
         else:
             files = None
         if merge:
             p1, p2 = repo.dirstate.parents()
             repo.setparents(p1, node)
             m = match.always(repo.root, '')
--- a/hgext/win32text.py
+++ b/hgext/win32text.py
@@ -45,17 +45,19 @@ from __future__ import absolute_import
 
 import re
 from mercurial.i18n import _
 from mercurial.node import (
     short,
 )
 from mercurial import (
     registrar,
-    util,
+)
+from mercurial.utils import (
+    stringutil,
 )
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
 # be specifying the version(s) of Mercurial they are tested with, or
 # leave the attribute unspecified.
 testedwith = 'ships-with-hg-core'
 
@@ -95,32 +97,32 @@ def dumbencode(s, cmd):
 def macdumbdecode(s, cmd, **kwargs):
     checknewline(s, '\r', **kwargs)
     return s.replace('\n', '\r')
 
 def macdumbencode(s, cmd):
     return s.replace('\r', '\n')
 
 def cleverdecode(s, cmd, **kwargs):
-    if not util.binary(s):
+    if not stringutil.binary(s):
         return dumbdecode(s, cmd, **kwargs)
     return s
 
 def cleverencode(s, cmd):
-    if not util.binary(s):
+    if not stringutil.binary(s):
         return dumbencode(s, cmd)
     return s
 
 def macdecode(s, cmd, **kwargs):
-    if not util.binary(s):
+    if not stringutil.binary(s):
         return macdumbdecode(s, cmd, **kwargs)
     return s
 
 def macencode(s, cmd):
-    if not util.binary(s):
+    if not stringutil.binary(s):
         return macdumbencode(s, cmd)
     return s
 
 _filters = {
     'dumbdecode:': dumbdecode,
     'dumbencode:': dumbencode,
     'cleverdecode:': cleverdecode,
     'cleverencode:': cleverencode,
@@ -141,17 +143,17 @@ def forbidnewline(ui, repo, hooktype, no
     tip = repo['tip']
     for rev in xrange(repo.changelog.tiprev(), repo[node].rev() - 1, -1):
         c = repo[rev]
         for f in c.files():
             if f in seen or f not in tip or f not in c:
                 continue
             seen.add(f)
             data = c[f].data()
-            if not util.binary(data) and newline in data:
+            if not stringutil.binary(data) and newline in data:
                 if not halt:
                     ui.warn(_('attempt to commit or push text file(s) '
                               'using %s line endings\n') %
                               newlinestr[newline])
                 ui.warn(_('in %s: %s\n') % (short(c.node()), f))
                 halt = True
     if halt and hooktype == 'pretxnchangegroup':
         crlf = newlinestr[newline].lower()
--- a/mercurial/branchmap.py
+++ b/mercurial/branchmap.py
@@ -17,16 +17,19 @@ from .node import (
 )
 from . import (
     encoding,
     error,
     pycompat,
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 calcsize = struct.calcsize
 pack_into = struct.pack_into
 unpack_from = struct.unpack_from
 
 def _filename(repo):
     """name of a branchcache file for a given repo or repoview"""
     filename = "branch2"
@@ -251,17 +254,17 @@ class branchcache(dict):
                                             encoding.fromlocal(label)))
             f.close()
             repo.ui.log('branchcache',
                         'wrote %s branch cache with %d labels and %d nodes\n',
                         repo.filtername, len(self), nodecount)
         except (IOError, OSError, error.Abort) as inst:
             # Abort may be raised by read only opener, so log and continue
             repo.ui.debug("couldn't write branch cache: %s\n" %
-                          util.forcebytestr(inst))
+                          stringutil.forcebytestr(inst))
 
     def update(self, repo, revgen):
         """Given a branchhead cache, self, that may have extra nodes or be
         missing heads, and a generator of nodes that are strictly a superset of
         heads missing, this function updates self to be correct.
         """
         starttime = util.timer()
         cl = repo.changelog
@@ -373,17 +376,17 @@ class revbranchcache(object):
                 self.branchinfo = self._branchinfo
 
         if self._names:
             try:
                 data = repo.cachevfs.read(_rbcrevs)
                 self._rbcrevs[:] = data
             except (IOError, OSError) as inst:
                 repo.ui.debug("couldn't read revision branch cache: %s\n" %
-                              util.forcebytestr(inst))
+                              stringutil.forcebytestr(inst))
         # remember number of good records on disk
         self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
                                len(repo.changelog))
         if self._rbcrevslen == 0:
             self._names = []
         self._rbcnamescount = len(self._names) # number of names read at
                                                # _rbcsnameslen
         self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
@@ -535,12 +538,12 @@ class revbranchcache(object):
                         f.seek(start)
                     f.truncate()
                 end = revs * _rbcrecsize
                 f.write(self._rbcrevs[start:end])
                 f.close()
                 self._rbcrevslen = revs
         except (IOError, OSError, error.Abort, error.LockError) as inst:
             repo.ui.debug("couldn't write revision branch cache%s: %s\n"
-                          % (step, util.forcebytestr(inst)))
+                          % (step, stringutil.forcebytestr(inst)))
         finally:
             if wlock is not None:
                 wlock.release()
--- a/mercurial/bundle2.py
+++ b/mercurial/bundle2.py
@@ -166,16 +166,19 @@ from . import (
     phases,
     pushkey,
     pycompat,
     streamclone,
     tags,
     url,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 _pack = struct.pack
 _unpack = struct.unpack
 
 _fstreamparamsize = '>i'
@@ -1086,17 +1089,17 @@ class bundlepart(object):
                 yield chunk
         except GeneratorExit:
             # GeneratorExit means that nobody is listening for our
             # results anyway, so just bail quickly rather than trying
             # to produce an error part.
             ui.debug('bundle2-generatorexit\n')
             raise
         except BaseException as exc:
-            bexc = util.forcebytestr(exc)
+            bexc = stringutil.forcebytestr(exc)
             # backup exception data for later
             ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
                      % bexc)
             tb = sys.exc_info()[2]
             msg = 'unexpected error: %s' % bexc
             interpart = bundlepart('error:abort', [('message', msg)],
                                    mandatory=False)
             interpart.id = 0
--- a/mercurial/changegroup.py
+++ b/mercurial/changegroup.py
@@ -23,16 +23,20 @@ from . import (
     dagutil,
     error,
     mdiff,
     phases,
     pycompat,
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 _CHANGEGROUPV1_DELTA_HEADER = "20s20s20s20s"
 _CHANGEGROUPV2_DELTA_HEADER = "20s20s20s20s20s"
 _CHANGEGROUPV3_DELTA_HEADER = ">20s20s20s20s20sH"
 
 # When narrowing is finalized and no longer subject to format changes,
 # we should move this to just "narrow" or similar.
 NARROW_REQUIREMENT = 'narrowhg-experimental'
 
@@ -509,17 +513,17 @@ class cg1packer(object):
         if bundlecaps is None:
             bundlecaps = set()
         self._bundlecaps = bundlecaps
         # experimental config: bundle.reorder
         reorder = repo.ui.config('bundle', 'reorder')
         if reorder == 'auto':
             reorder = None
         else:
-            reorder = util.parsebool(reorder)
+            reorder = stringutil.parsebool(reorder)
         self._repo = repo
         self._reorder = reorder
         self._progress = repo.ui.progress
         if self._repo.ui.verbose and not self._repo.ui.debugflag:
             self._verbosenote = self._repo.ui.note
         else:
             self._verbosenote = lambda s: None
 
--- a/mercurial/changelog.py
+++ b/mercurial/changelog.py
@@ -19,29 +19,32 @@ from .thirdparty import (
 
 from . import (
     encoding,
     error,
     pycompat,
     revlog,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 _defaultextra = {'branch': 'default'}
 
 def _string_escape(text):
     """
     >>> from .pycompat import bytechr as chr
     >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
     >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
     >>> s
     'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
     >>> res = _string_escape(s)
-    >>> s == util.unescapestr(res)
+    >>> s == stringutil.unescapestr(res)
     True
     """
     # subset of the string_escape codec
     text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
     return text.replace('\0', '\\0')
 
 def decodeextra(text):
     """
@@ -57,17 +60,17 @@ def decodeextra(text):
     extra = _defaultextra.copy()
     for l in text.split('\0'):
         if l:
             if '\\0' in l:
                 # fix up \0 without getting into trouble with \\0
                 l = l.replace('\\\\', '\\\\\n')
                 l = l.replace('\\0', '\0')
                 l = l.replace('\n', '')
-            k, v = util.unescapestr(l).split(':', 1)
+            k, v = stringutil.unescapestr(l).split(':', 1)
             extra[k] = v
     return extra
 
 def encodeextra(d):
     # keys must be sorted to produce a deterministic changelog entry
     items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
     return "\0".join(items)
 
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -43,17 +43,22 @@ from . import (
     scmutil,
     smartset,
     subrepoutil,
     templatekw,
     templater,
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+
+from .utils import (
+    dateutil,
+    stringutil,
+)
+
 stringio = util.stringio
 
 # templates of common command options
 
 dryrunopts = [
     ('n', 'dry-run', None,
      _('do not perform actions, just print output')),
 ]
@@ -957,19 +962,19 @@ def _buildfntemplate(pat, total=None, se
     for typ, start, end in templater.scantemplate(pat, raw=True):
         if typ != b'string':
             newname.append(pat[start:end])
             continue
         i = start
         while i < end:
             n = pat.find(b'%', i, end)
             if n < 0:
-                newname.append(util.escapestr(pat[i:end]))
+                newname.append(stringutil.escapestr(pat[i:end]))
                 break
-            newname.append(util.escapestr(pat[i:n]))
+            newname.append(stringutil.escapestr(pat[i:n]))
             if n + 2 > end:
                 raise error.Abort(_("incomplete format spec in output "
                                     "filename"))
             c = pat[n + 1:n + 2]
             i = n + 2
             try:
                 newname.append(expander[c])
             except KeyError:
@@ -1474,17 +1479,17 @@ def tryimportone(ui, repo, hunk, parents
                 branch = p1.branch()
             store = patch.filestore()
             try:
                 files = set()
                 try:
                     patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
                                     files, eolmode=None)
                 except error.PatchError as e:
-                    raise error.Abort(util.forcebytestr(e))
+                    raise error.Abort(stringutil.forcebytestr(e))
                 if opts.get('exact'):
                     editor = None
                 else:
                     editor = getcommiteditor(editform='import.bypass')
                 memctx = context.memctx(repo, (p1.node(), p2.node()),
                                             message,
                                             files=files,
                                             filectxfn=store,
--- a/mercurial/color.py
+++ b/mercurial/color.py
@@ -9,17 +9,20 @@ from __future__ import absolute_import
 
 import re
 
 from .i18n import _
 
 from . import (
     encoding,
     pycompat,
-    util
+)
+
+from .utils import (
+    stringutil,
 )
 
 try:
     import curses
     # Mapping from effect name to terminfo attribute name (or raw code) or
     # color number.  This will also force-load the curses module.
     _baseterminfoparams = {
         'none': (True, 'sgr0', ''),
@@ -195,17 +198,17 @@ def _modesetup(ui):
     if ui.plain('color'):
         return None
     config = ui.config('ui', 'color')
     if config == 'debug':
         return 'debug'
 
     auto = (config == 'auto')
     always = False
-    if not auto and util.parsebool(config):
+    if not auto and stringutil.parsebool(config):
         # We want the config to behave like a boolean, "on" is actually auto,
         # but "always" value is treated as a special case to reduce confusion.
         if ui.configsource('ui', 'color') == '--color' or config == 'always':
             always = True
         else:
             auto = True
 
     if not always and not auto:
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -56,17 +56,20 @@ from . import (
     server,
     streamclone,
     tags as tagsmod,
     templatekw,
     ui as uimod,
     util,
     wireprotoserver,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 release = lockmod.release
 
 table = {}
 table.update(debugcommandsmod.command._table)
 
 command = registrar.command(table)
 readonly = registrar.command.readonly
@@ -2464,17 +2467,17 @@ def grep(ui, repo, pattern, *pats, **opt
         if ui.quiet:
             datefmt = '%Y-%m-%d'
         else:
             datefmt = '%a %b %d %H:%M:%S %Y %1%2'
         found = False
         @util.cachefunc
         def binary():
             flog = getfile(fn)
-            return util.binary(flog.read(ctx.filenode(fn)))
+            return stringutil.binary(flog.read(ctx.filenode(fn)))
 
         fieldnamemap = {'filename': 'file', 'linenumber': 'line_number'}
         if opts.get('all'):
             iter = difflinestates(pstates, states)
         else:
             iter = [('', l) for l in states]
         for change, l in iter:
             fm.startitem()
@@ -3909,17 +3912,17 @@ def postincoming(ui, repo, modheads, opt
     :brev: a name, which might be a bookmark to be activated after updating
     """
     if modheads == 0:
         return
     if optupdate:
         try:
             return hg.updatetotally(ui, repo, checkout, brev)
         except error.UpdateAbort as inst:
-            msg = _("not updating: %s") % util.forcebytestr(inst)
+            msg = _("not updating: %s") % stringutil.forcebytestr(inst)
             hint = inst.hint
             raise error.UpdateAbort(msg, hint=hint)
     if modheads > 1:
         currentbranchheads = len(repo.branchheads())
         if currentbranchheads == modheads:
             ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
         elif currentbranchheads > 1:
             ui.status(_("(run 'hg heads .' to see heads, 'hg merge' to "
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -41,17 +41,20 @@ from . import (
     repoview,
     revlog,
     scmutil,
     sparse,
     subrepo,
     subrepoutil,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 propertycache = util.propertycache
 
 nonascii = re.compile(br'[^\x21-\x7f]').search
 
 class basectx(object):
     """A basectx object represents the common logic for its children:
     changectx: read-only context that is already present in the repo,
@@ -813,17 +816,17 @@ class basefilectx(object):
     def size(self):
         return len(self.data())
 
     def path(self):
         return self._path
 
     def isbinary(self):
         try:
-            return util.binary(self.data())
+            return stringutil.binary(self.data())
         except IOError:
             return False
     def isexec(self):
         return 'x' in self.flags()
     def islink(self):
         return 'l' in self.flags()
 
     def isabsent(self):
@@ -1495,17 +1498,18 @@ class workingctx(committablectx):
         # Symlink placeholders may get non-symlink-like contents
         # via user error or dereferencing by NFS or Samba servers,
         # so we filter out any placeholders that don't look like a
         # symlink
         sane = []
         for f in files:
             if self.flags(f) == 'l':
                 d = self[f].data()
-                if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
+                if (d == '' or len(d) >= 1024 or '\n' in d
+                    or stringutil.binary(d)):
                     self._repo.ui.debug('ignoring suspect symlink placeholder'
                                         ' "%s"\n' % f)
                     continue
             sane.append(f)
         return sane
 
     def _checklookup(self, files):
         # check for any possibly clean files
--- a/mercurial/crecord.py
+++ b/mercurial/crecord.py
@@ -18,16 +18,19 @@ import signal
 from .i18n import _
 from . import (
     encoding,
     error,
     patch as patchmod,
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 stringio = util.stringio
 
 # This is required for ncurses to display non-ASCII characters in default user
 # locale encoding correctly.  --immerrr
 locale.setlocale(locale.LC_ALL, u'')
 
 # patch comments based on the git one
 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
@@ -580,17 +583,17 @@ class curseschunkselector(object):
         self.colorpairs = {}
         # maps custom nicknames of color-pairs to curses color-pair values
         self.colorpairnames = {}
 
         # Honor color setting of ui section. Keep colored setup as
         # long as not explicitly set to a falsy value - especially,
         # when not set at all. This is to stay most compatible with
         # previous (color only) behaviour.
-        uicolor = util.parsebool(self.ui.config('ui', 'color'))
+        uicolor = stringutil.parsebool(self.ui.config('ui', 'color'))
         self.usecolor = uicolor is not False
 
         # the currently selected header, hunk, or hunk-line
         self.currentselecteditem = self.headerlist[0]
 
         # updated when printing out patch-display -- the 'lines' here are the
         # line positions *in the pad*, not on the screen.
         self.selecteditemstartline = 0
@@ -1053,17 +1056,17 @@ class curseschunkselector(object):
                     lines.append(s)
                     lastwidth = w
                 else:
                     lines[-1] += sep + s
                     lastwidth += w + len(sep)
         if len(lines) != self.numstatuslines:
             self.numstatuslines = len(lines)
             self.statuswin.resize(self.numstatuslines, self.xscreensize)
-        return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
+        return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
 
     def updatescreen(self):
         self.statuswin.erase()
         self.chunkpad.erase()
 
         printstring = self.printstring
 
         # print out the status lines at the top
--- a/mercurial/dagparser.py
+++ b/mercurial/dagparser.py
@@ -9,17 +9,19 @@ from __future__ import absolute_import
 
 import re
 import string
 
 from .i18n import _
 from . import (
     error,
     pycompat,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 def parsedag(desc):
     '''parses a DAG from a concise textual description; generates events
 
     "+n" is a linear run of n nodes based on the current default parent
     "." is a single node based on the current default parent
     "$" resets the default parent to -1 (implied at the start);
@@ -367,18 +369,18 @@ def dagtextlines(events,
                         yield '\n'
                     yield '@' + wrapstring(data)
                 elif kind == '#':
                     yield '#' + data
                     yield '\n'
                 else:
                     raise error.Abort(_("invalid event type in dag: "
                                         "('%s', '%s')")
-                                      % (util.escapestr(kind),
-                                         util.escapestr(data)))
+                                      % (stringutil.escapestr(kind),
+                                         stringutil.escapestr(data)))
         if run:
             yield '+%d' % run
 
     line = ''
     for part in gen():
         if part == '\n':
             if line:
                 yield line
--- a/mercurial/debugcommands.py
+++ b/mercurial/debugcommands.py
@@ -76,17 +76,20 @@ from . import (
     treediscovery,
     upgrade,
     url as urlmod,
     util,
     vfs as vfsmod,
     wireprotoframing,
     wireprotoserver,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 release = lockmod.release
 
 command = registrar.command()
 
 @command('debugancestor', [], _('[INDEX] REV1 REV2'), optionalrepo=True)
 def debugancestor(ui, repo, *args):
     """find the ancestor revision of two revisions in a given index"""
@@ -1136,17 +1139,17 @@ def debuginstall(ui, **opts):
     fm.startitem()
 
     # encoding
     fm.write('encoding', _("checking encoding (%s)...\n"), encoding.encoding)
     err = None
     try:
         codecs.lookup(pycompat.sysstr(encoding.encoding))
     except LookupError as inst:
-        err = util.forcebytestr(inst)
+        err = stringutil.forcebytestr(inst)
         problems += 1
     fm.condwrite(err, 'encodingerror', _(" %s\n"
                  " (check that your locale is properly set)\n"), err)
 
     # Python
     fm.write('pythonexe', _("checking Python executable (%s)\n"),
              pycompat.sysexecutable)
     fm.write('pythonver', _("checking Python version (%s)\n"),
@@ -1192,17 +1195,17 @@ def debuginstall(ui, **opts):
             from .cext import (
                 base85,
                 bdiff,
                 mpatch,
                 osutil,
             )
             dir(bdiff), dir(mpatch), dir(base85), dir(osutil) # quiet pyflakes
         except Exception as inst:
-            err = util.forcebytestr(inst)
+            err = stringutil.forcebytestr(inst)
             problems += 1
         fm.condwrite(err, 'extensionserror', " %s\n", err)
 
     compengines = util.compengines._engines.values()
     fm.write('compengines', _('checking registered compression engines (%s)\n'),
              fm.formatlist(sorted(e.name() for e in compengines),
                            name='compengine', fmt='%s', sep=', '))
     fm.write('compenginesavail', _('checking available compression engines '
@@ -1229,17 +1232,17 @@ def debuginstall(ui, **opts):
     if p:
         m = templater.templatepath("map-cmdline.default")
         if m:
             # template found, check if it is working
             err = None
             try:
                 templater.templater.frommapfile(m)
             except Exception as inst:
-                err = util.forcebytestr(inst)
+                err = stringutil.forcebytestr(inst)
                 p = None
             fm.condwrite(err, 'defaulttemplateerror', " %s\n", err)
         else:
             p = None
         fm.condwrite(p, 'defaulttemplate',
                      _("checking default template (%s)\n"), m)
         fm.condwrite(not m, 'defaulttemplatenotfound',
                      _(" template '%s' not found\n"), "default")
@@ -1266,17 +1269,17 @@ def debuginstall(ui, **opts):
         problems += 1
 
     # check username
     username = None
     err = None
     try:
         username = ui.username()
     except error.Abort as e:
-        err = util.forcebytestr(e)
+        err = stringutil.forcebytestr(e)
         problems += 1
 
     fm.condwrite(username, 'username',  _("checking username (%s)\n"), username)
     fm.condwrite(err, 'usernameerror', _("checking username...\n %s\n"
         " (specify a username in your configuration file)\n"), err)
 
     fm.condwrite(not problems, '',
                  _("no problems detected\n"))
@@ -1817,18 +1820,18 @@ def debugpushkey(ui, repopath, namespace
     target = hg.peer(ui, {}, repopath)
     if keyinfo:
         key, old, new = keyinfo
         r = target.pushkey(namespace, key, old, new)
         ui.status(pycompat.bytestr(r) + '\n')
         return not r
     else:
         for k, v in sorted(target.listkeys(namespace).iteritems()):
-            ui.write("%s\t%s\n" % (util.escapestr(k),
-                                   util.escapestr(v)))
+            ui.write("%s\t%s\n" % (stringutil.escapestr(k),
+                                   stringutil.escapestr(v)))
 
 @command('debugpvec', [], _('A B'))
 def debugpvec(ui, repo, a, b=None):
     ca = scmutil.revsingle(repo, a)
     cb = scmutil.revsingle(repo, b)
     pa = pvec.ctxpvec(ca)
     pb = pvec.ctxpvec(cb)
     if pa == pb:
@@ -2904,17 +2907,17 @@ def debugwireproto(ui, repo, path=None, 
     # Now perform actions based on the parsed wire language instructions.
     for action, lines in blocks:
         if action in ('raw', 'raw+'):
             if not stdin:
                 raise error.Abort(_('cannot call raw/raw+ on this peer'))
 
             # Concatenate the data together.
             data = ''.join(l.lstrip() for l in lines)
-            data = util.unescapestr(data)
+            data = stringutil.unescapestr(data)
             stdin.write(data)
 
             if action == 'raw+':
                 stdin.flush()
         elif action == 'flush':
             if not stdin:
                 raise error.Abort(_('cannot call flush on this peer'))
             stdin.flush()
@@ -2930,49 +2933,50 @@ def debugwireproto(ui, repo, path=None, 
                 # We need to allow empty values.
                 fields = line.lstrip().split(' ', 1)
                 if len(fields) == 1:
                     key = fields[0]
                     value = ''
                 else:
                     key, value = fields
 
-                args[key] = util.unescapestr(value)
+                args[key] = stringutil.unescapestr(value)
 
             if batchedcommands is not None:
                 batchedcommands.append((command, args))
                 continue
 
             ui.status(_('sending %s command\n') % command)
 
             if 'PUSHFILE' in args:
                 with open(args['PUSHFILE'], r'rb') as fh:
                     del args['PUSHFILE']
                     res, output = peer._callpush(command, fh,
                                                  **pycompat.strkwargs(args))
-                    ui.status(_('result: %s\n') % util.escapedata(res))
+                    ui.status(_('result: %s\n') % stringutil.escapedata(res))
                     ui.status(_('remote output: %s\n') %
-                              util.escapedata(output))
+                              stringutil.escapedata(output))
             else:
                 res = peer._call(command, **pycompat.strkwargs(args))
-                ui.status(_('response: %s\n') % util.escapedata(res))
+                ui.status(_('response: %s\n') % stringutil.escapedata(res))
 
         elif action == 'batchbegin':
             if batchedcommands is not None:
                 raise error.Abort(_('nested batchbegin not allowed'))
 
             batchedcommands = []
         elif action == 'batchsubmit':
             # There is a batching API we could go through. But it would be
             # difficult to normalize requests into function calls. It is easier
             # to bypass this layer and normalize to commands + args.
             ui.status(_('sending batch with %d sub-commands\n') %
                       len(batchedcommands))
             for i, chunk in enumerate(peer._submitbatch(batchedcommands)):
-                ui.status(_('response #%d: %s\n') % (i, util.escapedata(chunk)))
+                ui.status(_('response #%d: %s\n') %
+                          (i, stringutil.escapedata(chunk)))
 
             batchedcommands = None
 
         elif action.startswith('httprequest '):
             if not opener:
                 raise error.Abort(_('cannot use httprequest without an HTTP '
                                     'peer'))
 
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -36,16 +36,20 @@ from . import (
     profiling,
     pycompat,
     registrar,
     scmutil,
     ui as uimod,
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 unrecoverablewrite = registrar.command.unrecoverablewrite
 
 class request(object):
     def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
                  ferr=None, prereposetups=None):
         self.args = args
         self.ui = ui
         self.repo = repo
@@ -491,17 +495,17 @@ class cmdalias(object):
                                  blockedtag='alias_%s' % self.name)
             self.fn = fn
             return
 
         try:
             args = pycompat.shlexsplit(self.definition)
         except ValueError as inst:
             self.badalias = (_("error in definition for alias '%s': %s")
-                             % (self.name, util.forcebytestr(inst)))
+                             % (self.name, stringutil.forcebytestr(inst)))
             return
         earlyopts, args = _earlysplitopts(args)
         if earlyopts:
             self.badalias = (_("error in definition for alias '%s': %s may "
                                "only be given on the command line")
                              % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
                                                     [0])))
             return
@@ -618,17 +622,17 @@ def addaliases(ui, cmdtable):
 
 def _parse(ui, args):
     options = {}
     cmdoptions = {}
 
     try:
         args = fancyopts.fancyopts(args, commands.globalopts, options)
     except getopt.GetoptError as inst:
-        raise error.CommandError(None, util.forcebytestr(inst))
+        raise error.CommandError(None, stringutil.forcebytestr(inst))
 
     if args:
         cmd, args = args[0], args[1:]
         aliases, entry = cmdutil.findcmd(cmd, commands.table,
                                          ui.configbool("ui", "strict"))
         cmd = aliases[0]
         args = aliasargs(entry[0], args)
         defaults = ui.config("defaults", cmd)
@@ -642,17 +646,17 @@ def _parse(ui, args):
 
     # combine global options into local
     for o in commands.globalopts:
         c.append((o[0], o[1], options[o[1]], o[3]))
 
     try:
         args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
     except getopt.GetoptError as inst:
-        raise error.CommandError(cmd, util.forcebytestr(inst))
+        raise error.CommandError(cmd, stringutil.forcebytestr(inst))
 
     # separate global options back out
     for o in commands.globalopts:
         n = o[1]
         options[n] = cmdoptions[n]
         del cmdoptions[n]
 
     return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
@@ -867,17 +871,17 @@ def _dispatch(req):
         # setup color handling before pager, because setting up pager
         # might cause incorrect console information
         coloropt = options['color']
         for ui_ in uis:
             if coloropt:
                 ui_.setconfig('ui', 'color', coloropt, '--color')
             color.setup(ui_)
 
-        if util.parsebool(options['pager']):
+        if stringutil.parsebool(options['pager']):
             # ui.pager() expects 'internal-always-' prefix in this case
             ui.pager('internal-always-' + cmd)
         elif options['pager'] != 'auto':
             for ui_ in uis:
                 ui_.disablepager()
 
         if options['version']:
             return commands.version_(ui)
@@ -963,17 +967,17 @@ def _exceptionwarning(ui):
     # of date) will be clueful enough to notice the implausible
     # version number and try updating.
     ct = util.versiontuple(n=2)
     worst = None, ct, ''
     if ui.config('ui', 'supportcontact') is None:
         for name, mod in extensions.extensions():
             # 'testedwith' should be bytes, but not all extensions are ported
             # to py3 and we don't want UnicodeException because of that.
-            testedwith = util.forcebytestr(getattr(mod, 'testedwith', ''))
+            testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
             report = getattr(mod, 'buglink', _('the extension author.'))
             if not testedwith.strip():
                 # We found an untested extension. It's likely the culprit.
                 worst = name, 'unknown', report
                 break
 
             # Never blame on extensions bundled with Mercurial.
             if extensions.ismoduleinternal(mod):
@@ -985,17 +989,18 @@ def _exceptionwarning(ui):
 
             lower = [t for t in tested if t < ct]
             nearest = max(lower or tested)
             if worst[0] is None or nearest < worst[1]:
                 worst = name, nearest, report
     if worst[0] is not None:
         name, testedwith, report = worst
         if not isinstance(testedwith, (bytes, str)):
-            testedwith = '.'.join([util.forcebytestr(c) for c in testedwith])
+            testedwith = '.'.join([stringutil.forcebytestr(c)
+                                   for c in testedwith])
         warning = (_('** Unknown exception encountered with '
                      'possibly-broken third-party extension %s\n'
                      '** which supports versions %s of Mercurial.\n'
                      '** Please disable %s and try your action again.\n'
                      '** If that fixes the bug please report it to %s\n')
                    % (name, testedwith, name, report))
     else:
         bugtracker = ui.config('ui', 'supportcontact')
--- a/mercurial/exchange.py
+++ b/mercurial/exchange.py
@@ -30,16 +30,19 @@ from . import (
     pushkey,
     pycompat,
     scmutil,
     sslutil,
     streamclone,
     url as urlmod,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 # Maps bundle version human names to changegroup versions.
 _bundlespeccgversions = {'v1': '01',
                          'v2': '02',
                          'packed1': 's1',
@@ -2175,17 +2178,17 @@ def filterclonebundleentries(repo, entri
                     continue
 
             except error.InvalidBundleSpecification as e:
                 repo.ui.debug(str(e) + '\n')
                 continue
             except error.UnsupportedBundleSpecification as e:
                 repo.ui.debug('filtering %s because unsupported bundle '
                               'spec: %s\n' % (
-                                  entry['URL'], util.forcebytestr(e)))
+                                  entry['URL'], stringutil.forcebytestr(e)))
                 continue
         # If we don't have a spec and requested a stream clone, we don't know
         # what the entry is so don't attempt to apply it.
         elif streamclonerequested:
             repo.ui.debug('filtering %s because cannot determine if a stream '
                           'clone bundle\n' % entry['URL'])
             continue
 
@@ -2281,14 +2284,14 @@ def trypullbundlefromurl(ui, repo, url):
 
             if isinstance(cg, streamclone.streamcloneapplier):
                 cg.apply(repo)
             else:
                 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
             return True
         except urlerr.httperror as e:
             ui.warn(_('HTTP error fetching bundle: %s\n') %
-                    util.forcebytestr(e))
+                    stringutil.forcebytestr(e))
         except urlerr.urlerror as e:
             ui.warn(_('error fetching bundle: %s\n') %
-                    util.forcebytestr(e.reason))
+                    stringutil.forcebytestr(e.reason))
 
         return False
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -20,16 +20,20 @@ from .i18n import (
 from . import (
     cmdutil,
     configitems,
     error,
     pycompat,
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 _extensions = {}
 _disabledextensions = {}
 _aftercallbacks = {}
 _order = []
 _builtin = {
     'hbisect',
     'bookmarks',
     'color',
@@ -113,28 +117,28 @@ def _importext(name, path=None, reportfu
                     reportfunc(err, "hgext3rd.%s" % name, name)
                 mod = _importh(name)
     return mod
 
 def _reportimporterror(ui, err, failed, next):
     # note: this ui.debug happens before --debug is processed,
     #       Use --config ui.debug=1 to see them.
     ui.debug('could not import %s (%s): trying %s\n'
-             % (failed, util.forcebytestr(err), next))
+             % (failed, stringutil.forcebytestr(err), next))
     if ui.debugflag:
         ui.traceback()
 
 def _rejectunicode(name, xs):
     if isinstance(xs, (list, set, tuple)):
         for x in xs:
             _rejectunicode(name, x)
     elif isinstance(xs, dict):
         for k, v in xs.items():
             _rejectunicode(name, k)
-            _rejectunicode(b'%s.%s' % (name, util.forcebytestr(k)), v)
+            _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
     elif isinstance(xs, type(u'')):
         raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
                                      hint="use b'' to make it byte string")
 
 # attributes set by registrar.command
 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
 
 def _validatecmdtable(ui, cmdtable):
@@ -193,34 +197,34 @@ def load(ui, name, path):
 
 def _runuisetup(name, ui):
     uisetup = getattr(_extensions[name], 'uisetup', None)
     if uisetup:
         try:
             uisetup(ui)
         except Exception as inst:
             ui.traceback(force=True)
-            msg = util.forcebytestr(inst)
+            msg = stringutil.forcebytestr(inst)
             ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
             return False
     return True
 
 def _runextsetup(name, ui):
     extsetup = getattr(_extensions[name], 'extsetup', None)
     if extsetup:
         try:
             try:
                 extsetup(ui)
             except TypeError:
                 if pycompat.getargspec(extsetup).args:
                     raise
                 extsetup() # old extsetup with no ui argument
         except Exception as inst:
             ui.traceback(force=True)
-            msg = util.forcebytestr(inst)
+            msg = stringutil.forcebytestr(inst)
             ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
             return False
     return True
 
 def loadall(ui, whitelist=None):
     result = ui.configitems("extensions")
     if whitelist is not None:
         result = [(k, v) for (k, v) in result if k in whitelist]
@@ -228,17 +232,17 @@ def loadall(ui, whitelist=None):
     for (name, path) in result:
         if path:
             if path[0:1] == '!':
                 _disabledextensions[name] = path[1:]
                 continue
         try:
             load(ui, name, path)
         except Exception as inst:
-            msg = util.forcebytestr(inst)
+            msg = stringutil.forcebytestr(inst)
             if path:
                 ui.warn(_("*** failed to import extension %s from %s: %s\n")
                         % (name, path, msg))
             else:
                 ui.warn(_("*** failed to import extension %s: %s\n")
                         % (name, msg))
             if isinstance(inst, error.Hint) and inst.hint:
                 ui.warn(_("*** (%s)\n") % inst.hint)
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -26,16 +26,20 @@ from . import (
     scmutil,
     simplemerge,
     tagmerge,
     templatekw,
     templater,
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 def _toolstr(ui, tool, part, *args):
     return ui.config("merge-tools", tool + "." + part, *args)
 
 def _toolbool(ui, tool, part,*args):
     return ui.configbool("merge-tools", tool + "." + part, *args)
 
 def _toollist(ui, tool, part):
     return ui.configlist("merge-tools", tool + "." + part)
@@ -568,17 +572,17 @@ def _formatconflictmarker(ctx, template,
 
     label = ('%s:' % label).ljust(pad + 1)
     mark = '%s %s' % (label, templateresult)
 
     if mark:
         mark = mark.splitlines()[0] # split for safety
 
     # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
-    return util.ellipsis(mark, 80 - 8)
+    return stringutil.ellipsis(mark, 80 - 8)
 
 _defaultconflictlabels = ['local', 'other']
 
 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
     """Formats the given labels using the conflict marker template.
 
     Returns a list of formatted labels.
     """
--- a/mercurial/fileset.py
+++ b/mercurial/fileset.py
@@ -15,16 +15,19 @@ from . import (
     match as matchmod,
     merge,
     parser,
     pycompat,
     registrar,
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
     "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
     ":": (15, None, None, ("kindpat", 15), None),
     "-": (5, None, ("negate", 19), ("minus", 5), None),
     "not": (10, None, ("not", 10), None, None),
     "!": (10, None, ("not", 10), None, None),
@@ -440,17 +443,17 @@ def eol(mctx, x):
     """
 
     # i18n: "eol" is a keyword
     enc = getstring(x, _("eol requires a style name"))
 
     s = []
     for f in mctx.existing():
         d = mctx.ctx[f].data()
-        if util.binary(d):
+        if stringutil.binary(d):
             continue
         if (enc == 'dos' or enc == 'win') and '\r\n' in d:
             s.append(f)
         elif enc == 'unix' and re.search('(?<!\r)\n', d):
             s.append(f)
         elif enc == 'mac' and re.search('\r(?!\n)', d):
             s.append(f)
     return s
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -43,16 +43,20 @@ from . import (
     ui as uimod,
     unionrepo,
     url,
     util,
     verify as verifymod,
     vfs as vfsmod,
 )
 
+from .utils import (
+    stringutil,
+)
+
 release = lock.release
 
 # shared features
 sharedbookmarks = 'bookmarks'
 
 def _local(path):
     path = util.expandpath(util.urllocalpath(path))
     return (os.path.isfile(path) and bundlerepo or localrepo)
@@ -265,17 +269,17 @@ def share(ui, source, dest=None, update=
     if relative:
         try:
             sharedpath = os.path.relpath(sharedpath, destvfs.base)
             requirements += 'relshared\n'
         except (IOError, ValueError) as e:
             # ValueError is raised on Windows if the drive letters differ on
             # each path
             raise error.Abort(_('cannot calculate relative path'),
-                              hint=util.forcebytestr(e))
+                              hint=stringutil.forcebytestr(e))
     else:
         requirements += 'shared\n'
 
     destvfs.write('requires', requirements)
     destvfs.write('sharedpath', sharedpath)
 
     r = repository(ui, destwvfs.base)
     postshare(srcrepo, r, bookmarks=bookmarks, defaultpath=defaultpath)
--- a/mercurial/hgweb/webcommands.py
+++ b/mercurial/hgweb/webcommands.py
@@ -31,17 +31,20 @@ from .. import (
     error,
     graphmod,
     pycompat,
     revset,
     revsetlang,
     scmutil,
     smartset,
     templater,
-    util,
+)
+
+from ..utils import (
+    stringutil,
 )
 
 from . import (
     webutil,
 )
 
 __all__ = []
 commands = {}
@@ -116,17 +119,17 @@ def rawfile(web):
             raise inst
 
     path = fctx.path()
     text = fctx.data()
     mt = 'application/binary'
     if guessmime:
         mt = mimetypes.guess_type(path)[0]
         if mt is None:
-            if util.binary(text):
+            if stringutil.binary(text):
                 mt = 'application/binary'
             else:
                 mt = 'text/plain'
     if mt.startswith('text/'):
         mt += '; charset="%s"' % encoding.encoding
 
     web.res.headers['Content-Type'] = mt
     filename = (path.rpartition('/')[-1]
@@ -136,17 +139,17 @@ def rawfile(web):
     return web.res.sendresponse()
 
 def _filerevision(web, fctx):
     f = fctx.path()
     text = fctx.data()
     parity = paritygen(web.stripecount)
     ishead = fctx.filerev() in fctx.filelog().headrevs()
 
-    if util.binary(text):
+    if stringutil.binary(text):
         mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
         text = '(binary:%s)' % mt
 
     def lines():
         for lineno, t in enumerate(text.splitlines(True)):
             yield {"line": t,
                    "lineid": "l%d" % (lineno + 1),
                    "linenumber": "% 6d" % (lineno + 1),
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -33,16 +33,20 @@ from .. import (
     pathutil,
     pycompat,
     templatefilters,
     templatekw,
     ui as uimod,
     util,
 )
 
+from ..utils import (
+    stringutil,
+)
+
 def up(p):
     if p[0:1] != "/":
         p = "/" + p
     if p[-1:] == "/":
         p = p[:-1]
     up = os.path.dirname(p)
     if up == "/":
         return "/"
@@ -175,17 +179,17 @@ class _siblings(object):
 
 def difffeatureopts(req, ui, section):
     diffopts = patch.difffeatureopts(ui, untrusted=True,
                                      section=section, whitespace=True)
 
     for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
         v = req.qsparams.get(k)
         if v is not None:
-            v = util.parsebool(v)
+            v = stringutil.parsebool(v)
             setattr(diffopts, k, v if v is not None else True)
 
     return diffopts
 
 def annotate(req, fctx, ui):
     diffopts = difffeatureopts(req, ui, 'annotate')
     return fctx.annotate(follow=True, diffopts=diffopts)
 
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -59,16 +59,19 @@ from . import (
     store,
     subrepoutil,
     tags as tagsmod,
     transaction,
     txnutil,
     util,
     vfs as vfsmod,
 )
+from .utils import (
+    stringutil,
+)
 
 release = lockmod.release
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 # set of (path, vfs-location) tuples. vfs-location is:
 # - 'plain for vfs relative paths
 # - '' for svfs relative paths
@@ -258,17 +261,17 @@ class localpeer(repository.peer):
                     for out in output:
                         bundler.addpart(out)
                     stream = util.chunkbuffer(bundler.getchunks())
                     b = bundle2.getunbundler(self.ui, stream)
                     bundle2.processbundle(self._repo, b)
                 raise
         except error.PushRaced as exc:
             raise error.ResponseError(_('push failed:'),
-                                      util.forcebytestr(exc))
+                                      stringutil.forcebytestr(exc))
 
     # End of _basewirecommands interface.
 
     # Begin of peer interface.
 
     def iterbatch(self):
         return peer.localiterbatcher(self)
 
--- a/mercurial/logcmdutil.py
+++ b/mercurial/logcmdutil.py
@@ -30,17 +30,20 @@ from . import (
     revset,
     revsetlang,
     scmutil,
     smartset,
     templatekw,
     templater,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 def getlimit(opts):
     """get the log limit according to option -l/--limit"""
     limit = opts.get('limit')
     if limit:
         try:
             limit = int(limit)
         except ValueError:
@@ -255,17 +258,18 @@ class changesetprinter(object):
         if copies and self.ui.verbose:
             copies = ['%s (%s)' % c for c in copies]
             self.ui.write(columns['copies'] % ' '.join(copies),
                           label='ui.note log.copies')
 
         extra = ctx.extra()
         if extra and self.ui.debugflag:
             for key, value in sorted(extra.items()):
-                self.ui.write(columns['extra'] % (key, util.escapestr(value)),
+                self.ui.write(columns['extra']
+                              % (key, stringutil.escapestr(value)),
                               label='ui.debug log.extra')
 
         description = ctx.description().strip()
         if description:
             if self.ui.verbose:
                 self.ui.write(_("description:\n"),
                               label='ui.note log.description')
                 self.ui.write(description,
--- a/mercurial/mail.py
+++ b/mercurial/mail.py
@@ -19,16 +19,19 @@ import time
 from .i18n import _
 from . import (
     encoding,
     error,
     pycompat,
     sslutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class STARTTLS(smtplib.SMTP):
     '''Derived class to verify the peer certificate for STARTTLS.
 
     This class allows to pass any keyword arguments to SSL socket creation.
     '''
     def __init__(self, ui, host=None, **kwargs):
         smtplib.SMTP.__init__(self, **kwargs)
@@ -76,17 +79,17 @@ class SMTPS(smtplib.SMTP):
         self.file = smtplib.SSLFakeFile(new_socket)
         return new_socket
 
 def _smtp(ui):
     '''build an smtp connection and return a function to send mail'''
     local_hostname = ui.config('smtp', 'local_hostname')
     tls = ui.config('smtp', 'tls')
     # backward compatible: when tls = true, we use starttls.
-    starttls = tls == 'starttls' or util.parsebool(tls)
+    starttls = tls == 'starttls' or stringutil.parsebool(tls)
     smtps = tls == 'smtps'
     if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
         raise error.Abort(_("can't use TLS: Python SSL support not installed"))
     mailhost = ui.config('smtp', 'host')
     if not mailhost:
         raise error.Abort(_('smtp.host not configured - cannot send mail'))
     if smtps:
         ui.note(_('(using smtps)\n'))
@@ -132,18 +135,18 @@ def _smtp(ui):
         except smtplib.SMTPException as inst:
             raise error.Abort(inst)
 
     return send
 
 def _sendmail(ui, sender, recipients, msg):
     '''send mail using sendmail.'''
     program = ui.config('email', 'method')
-    cmdline = '%s -f %s %s' % (program, util.email(sender),
-                               ' '.join(map(util.email, recipients)))
+    cmdline = '%s -f %s %s' % (program, stringutil.email(sender),
+                               ' '.join(map(stringutil.email, recipients)))
     ui.note(_('sending mail: %s\n') % cmdline)
     fp = util.popen(cmdline, 'w')
     fp.write(msg)
     ret = fp.close()
     if ret:
         raise error.Abort('%s %s' % (
             os.path.basename(program.split(None, 1)[0]),
             util.explainexit(ret)[0]))
--- a/mercurial/match.py
+++ b/mercurial/match.py
@@ -14,16 +14,19 @@ import re
 from .i18n import _
 from . import (
     encoding,
     error,
     pathutil,
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 allpatternkinds = ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
                    'listfile', 'listfile0', 'set', 'include', 'subinclude',
                    'rootfilesin')
 cwdrelativepatternkinds = ('relpath', 'glob')
 
 propertycache = util.propertycache
 
@@ -222,17 +225,17 @@ def _donormalize(patterns, default, root
                 for k, p, source in _donormalize(includepats, default,
                                                  root, cwd, auditor, warn):
                     kindpats.append((k, p, source or pat))
             except error.Abort as inst:
                 raise error.Abort('%s: %s' % (pat, inst[0]))
             except IOError as inst:
                 if warn:
                     warn(_("skipping unreadable pattern file '%s': %s\n") %
-                         (pat, util.forcebytestr(inst.strerror)))
+                         (pat, stringutil.forcebytestr(inst.strerror)))
             continue
         # else: re or relre - which cannot be normalized
         kindpats.append((kind, pat, ''))
     return kindpats
 
 class basematcher(object):
 
     def __init__(self, root, cwd, badfn=None, relativeuipath=True):
--- a/mercurial/minirst.py
+++ b/mercurial/minirst.py
@@ -22,17 +22,19 @@ from __future__ import absolute_import
 
 import re
 
 from .i18n import _
 from . import (
     encoding,
     pycompat,
     url,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 def section(s):
     return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
 
 def subsection(s):
     return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
 
@@ -454,37 +456,37 @@ def findadmonitions(blocks, admonitions=
 
 def formatoption(block, width):
     desc = ' '.join(map(bytes.strip, block['lines']))
     colwidth = encoding.colwidth(block['optstr'])
     usablewidth = width - 1
     hanging = block['optstrwidth']
     initindent = '%s%s  ' % (block['optstr'], ' ' * ((hanging - colwidth)))
     hangindent = ' ' * (encoding.colwidth(initindent) + 1)
-    return ' %s\n' % (util.wrap(desc, usablewidth,
-                                           initindent=initindent,
-                                           hangindent=hangindent))
+    return ' %s\n' % (stringutil.wrap(desc, usablewidth,
+                                      initindent=initindent,
+                                      hangindent=hangindent))
 
 def formatblock(block, width):
     """Format a block according to width."""
     if width <= 0:
         width = 78
     indent = ' ' * block['indent']
     if block['type'] == 'admonition':
         admonition = _admonitiontitles[block['admonitiontitle']]
         if not block['lines']:
             return indent + admonition + '\n'
         hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
 
         defindent = indent + hang * ' '
         text = ' '.join(map(bytes.strip, block['lines']))
         return '%s\n%s\n' % (indent + admonition,
-                             util.wrap(text, width=width,
-                                       initindent=defindent,
-                                       hangindent=defindent))
+                             stringutil.wrap(text, width=width,
+                                             initindent=defindent,
+                                             hangindent=defindent))
     if block['type'] == 'margin':
         return '\n'
     if block['type'] == 'literal':
         indent += '  '
         return indent + ('\n' + indent).join(block['lines']) + '\n'
     if block['type'] == 'section':
         underline = encoding.colwidth(block['lines'][0]) * block['underline']
         return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
@@ -498,30 +500,32 @@ def formatblock(block, width):
         hang = ' ' * (len(indent) + span - widths[-1])
 
         for row in table:
             l = []
             for w, v in zip(widths, row):
                 pad = ' ' * (w - encoding.colwidth(v))
                 l.append(v + pad)
             l = ' '.join(l)
-            l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
+            l = stringutil.wrap(l, width=width,
+                                initindent=indent,
+                                hangindent=hang)
             if not text and block['header']:
                 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
             else:
                 text += l + "\n"
         return text
     if block['type'] == 'definition':
         term = indent + block['lines'][0]
         hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
         defindent = indent + hang * ' '
         text = ' '.join(map(bytes.strip, block['lines'][1:]))
-        return '%s\n%s\n' % (term, util.wrap(text, width=width,
-                                             initindent=defindent,
-                                             hangindent=defindent))
+        return '%s\n%s\n' % (term, stringutil.wrap(text, width=width,
+                                                   initindent=defindent,
+                                                   hangindent=defindent))
     subindent = indent
     if block['type'] == 'bullet':
         if block['lines'][0].startswith('| '):
             # Remove bullet for line blocks and add no extra
             # indentation.
             block['lines'][0] = block['lines'][0][2:]
         else:
             m = _bulletre.match(block['lines'][0])
@@ -535,19 +539,19 @@ def formatblock(block, width):
         else:
             # key fits within field width
             key = key.ljust(_fieldwidth)
         block['lines'][0] = key + block['lines'][0]
     elif block['type'] == 'option':
         return formatoption(block, width)
 
     text = ' '.join(map(bytes.strip, block['lines']))
-    return util.wrap(text, width=width,
-                     initindent=indent,
-                     hangindent=subindent) + '\n'
+    return stringutil.wrap(text, width=width,
+                           initindent=indent,
+                           hangindent=subindent) + '\n'
 
 def formathtml(blocks):
     """Format RST blocks as HTML"""
 
     out = []
     headernest = ''
     listnest = []
 
--- a/mercurial/parser.py
+++ b/mercurial/parser.py
@@ -20,16 +20,19 @@ from __future__ import absolute_import, 
 
 from .i18n import _
 from . import (
     encoding,
     error,
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class parser(object):
     def __init__(self, elements, methods=None):
         self._elements = elements
         self._methods = methods
         self.current = None
     def _advance(self):
         'advance the tokenizer'
@@ -185,24 +188,24 @@ def buildargsdict(trees, funcname, argsp
             raise error.ParseError(_("%(func)s got multiple values for keyword "
                                      "argument '%(key)s'")
                                    % {'func': funcname, 'key': k})
         d[k] = x[2]
     return args
 
 def unescapestr(s):
     try:
-        return util.unescapestr(s)
+        return stringutil.unescapestr(s)
     except ValueError as e:
         # mangle Python's exception into our format
         raise error.ParseError(pycompat.bytestr(e).lower())
 
 def _brepr(obj):
     if isinstance(obj, bytes):
-        return b"'%s'" % util.escapestr(obj)
+        return b"'%s'" % stringutil.escapestr(obj)
     return encoding.strtolocal(repr(obj))
 
 def _prettyformat(tree, leafnodes, level, lines):
     if not isinstance(tree, tuple):
         lines.append((level, _brepr(tree)))
     elif tree[0] in leafnodes:
         rs = map(_brepr, tree[1:])
         lines.append((level, '(%s %s)' % (tree[0], ' '.join(rs))))
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -35,17 +35,20 @@ from . import (
     pathutil,
     policy,
     pycompat,
     scmutil,
     similar,
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 diffhelpers = policy.importmod(r'diffhelpers')
 stringio = util.stringio
 
 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
 tabsplitter = re.compile(br'(\t+|[^\t]+)')
 _nonwordre = re.compile(br'([^a-zA-Z0-9_\x80-\xff])')
 
@@ -1456,17 +1459,17 @@ class binhunk(object):
             if l <= 'Z' and l >= 'A':
                 l = ord(l) - ord('A') + 1
             else:
                 l = ord(l) - ord('a') + 27
             try:
                 dec.append(util.b85decode(line[1:])[:l])
             except ValueError as e:
                 raise PatchError(_('could not decode "%s" binary patch: %s')
-                                 % (self._fname, util.forcebytestr(e)))
+                                 % (self._fname, stringutil.forcebytestr(e)))
             line = getline(lr, self.hunk)
         text = zlib.decompress(''.join(dec))
         if len(text) != size:
             raise PatchError(_('"%s" length is %d bytes, should be %d')
                              % (self._fname, len(text), size))
         self.text = text
 
 def parsefilename(str):
--- a/mercurial/repair.py
+++ b/mercurial/repair.py
@@ -21,16 +21,19 @@ from . import (
     changegroup,
     discovery,
     error,
     exchange,
     obsolete,
     obsutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 def backupbundle(repo, bases, heads, node, suffix, compress=True,
                  obsolescence=True):
     """create a bundle with the specified revisions as a backup"""
 
     backupdir = "strip-backup"
     vfs = repo.vfs
     if not vfs.isdir(backupdir):
@@ -231,17 +234,18 @@ def strip(ui, repo, nodelist, backup=Tru
 
         # remove undo files
         for undovfs, undofile in repo.undofiles():
             try:
                 undovfs.unlink(undofile)
             except OSError as e:
                 if e.errno != errno.ENOENT:
                     ui.warn(_('error removing %s: %s\n') %
-                            (undovfs.join(undofile), util.forcebytestr(e)))
+                            (undovfs.join(undofile),
+                             stringutil.forcebytestr(e)))
 
     except: # re-raises
         if backupfile:
             ui.warn(_("strip failed, backup bundle stored in '%s'\n")
                     % vfs.join(backupfile))
         if tmpbundlefile:
             ui.warn(_("strip failed, unrecovered changes stored in '%s'\n")
                     % vfs.join(tmpbundlefile))
--- a/mercurial/revlog.py
+++ b/mercurial/revlog.py
@@ -40,16 +40,19 @@ from . import (
     ancestor,
     error,
     mdiff,
     policy,
     pycompat,
     templatefilters,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 parsers = policy.importmod(r'parsers')
 
 # Aliased for performance.
 _zlibdecompress = zlib.decompress
 
 # revlog header flags
 REVLOGV0 = 0
@@ -2012,17 +2015,17 @@ class revlog(object):
         # compressed chunks. And this matters for changelog and manifest reads.
         t = data[0:1]
 
         if t == 'x':
             try:
                 return _zlibdecompress(data)
             except zlib.error as e:
                 raise RevlogError(_('revlog decompress error: %s') %
-                                  util.forcebytestr(e))
+                                  stringutil.forcebytestr(e))
         # '\0' is more common than 'u' so it goes first.
         elif t == '\0':
             return data
         elif t == 'u':
             return util.buffer(data, 1)
 
         try:
             compressor = self._decompressors[t]
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -26,17 +26,20 @@ from . import (
     registrar,
     repoview,
     revsetlang,
     scmutil,
     smartset,
     stack,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 # helpers for processing parsed tree
 getsymbol = revsetlang.getsymbol
 getstring = revsetlang.getstring
 getinteger = revsetlang.getinteger
 getboolean = revsetlang.getboolean
 getlist = revsetlang.getlist
 getrange = revsetlang.getrange
@@ -442,17 +445,17 @@ def bookmark(repo, subset, x):
     Pattern matching is supported for `name`. See :hg:`help revisions.patterns`.
     """
     # i18n: "bookmark" is a keyword
     args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
     if args:
         bm = getstring(args[0],
                        # i18n: "bookmark" is a keyword
                        _('the argument to bookmark must be a string'))
-        kind, pattern, matcher = util.stringmatcher(bm)
+        kind, pattern, matcher = stringutil.stringmatcher(bm)
         bms = set()
         if kind == 'literal':
             bmrev = repo._bookmarks.get(pattern, None)
             if not bmrev:
                 raise error.RepoLookupError(_("bookmark '%s' does not exist")
                                             % pattern)
             bms.add(repo[bmrev].rev())
         else:
@@ -487,17 +490,17 @@ def branch(repo, subset, x):
             return repo[r].branch()
 
     try:
         b = getstring(x, '')
     except error.ParseError:
         # not a string, but another revspec, e.g. tip()
         pass
     else:
-        kind, pattern, matcher = util.stringmatcher(b)
+        kind, pattern, matcher = stringutil.stringmatcher(b)
         if kind == 'literal':
             # note: falls through to the revspec case if no branch with
             # this name exists and pattern kind is not specified explicitly
             if pattern in repo.branchmap():
                 return subset.filter(lambda r: matcher(getbranch(r)),
                                      condrepr=('<branch %r>', b))
             if b.startswith('literal:'):
                 raise error.RepoLookupError(_("branch '%s' does not exist")
@@ -814,17 +817,17 @@ def extra(repo, subset, x):
     label = getstring(args['label'], _('first argument to extra must be '
                                        'a string'))
     value = None
 
     if 'value' in args:
         # i18n: "extra" is a keyword
         value = getstring(args['value'], _('second argument to extra must be '
                                            'a string'))
-        kind, value, matcher = util.stringmatcher(value)
+        kind, value, matcher = stringutil.stringmatcher(value)
 
     def _matchvalue(r):
         extra = repo[r].extra()
         return label in extra and (value is None or matcher(extra[label]))
 
     return subset.filter(lambda r: _matchvalue(r),
                          condrepr=('<extra[%r] %r>', label, value))
 
@@ -1009,17 +1012,17 @@ def grep(repo, subset, x):
     to ensure special escape characters are handled correctly. Unlike
     ``keyword(string)``, the match is case-sensitive.
     """
     try:
         # i18n: "grep" is a keyword
         gr = re.compile(getstring(x, _("grep requires a string")))
     except re.error as e:
         raise error.ParseError(
-            _('invalid match pattern: %s') % util.forcebytestr(e))
+            _('invalid match pattern: %s') % stringutil.forcebytestr(e))
 
     def matches(x):
         c = repo[x]
         for e in c.files() + [c.user(), c.description()]:
             if gr.search(e):
                 return True
         return False
 
@@ -1281,17 +1284,17 @@ def named(repo, subset, x):
     :hg:`help revisions.patterns`.
     """
     # i18n: "named" is a keyword
     args = getargs(x, 1, 1, _('named requires a namespace argument'))
 
     ns = getstring(args[0],
                    # i18n: "named" is a keyword
                    _('the argument to named must be a string'))
-    kind, pattern, matcher = util.stringmatcher(ns)
+    kind, pattern, matcher = stringutil.stringmatcher(ns)
     namespaces = set()
     if kind == 'literal':
         if pattern not in repo.names:
             raise error.RepoLookupError(_("namespace '%s' does not exist")
                                         % ns)
         namespaces.add(repo.names[pattern])
     else:
         for name, ns in repo.names.iteritems():
@@ -1937,17 +1940,17 @@ def subrepo(repo, subset, x):
     args = getargs(x, 0, 1, _('subrepo takes at most one argument'))
     pat = None
     if len(args) != 0:
         pat = getstring(args[0], _("subrepo requires a pattern"))
 
     m = matchmod.exact(repo.root, repo.root, ['.hgsubstate'])
 
     def submatches(names):
-        k, p, m = util.stringmatcher(pat)
+        k, p, m = stringutil.stringmatcher(pat)
         for name in names:
             if m(name):
                 yield name
 
     def matches(x):
         c = repo[x]
         s = repo.status(c.p1().node(), c.node(), match=m)
 
@@ -1990,18 +1993,18 @@ def _mapbynodefunc(repo, s, f):
 def successors(repo, subset, x):
     """All successors for set, including the given set themselves"""
     s = getset(repo, fullreposet(repo), x)
     f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes)
     d = _mapbynodefunc(repo, s, f)
     return subset & d
 
 def _substringmatcher(pattern, casesensitive=True):
-    kind, pattern, matcher = util.stringmatcher(pattern,
-                                                casesensitive=casesensitive)
+    kind, pattern, matcher = stringutil.stringmatcher(
+        pattern, casesensitive=casesensitive)
     if kind == 'literal':
         if not casesensitive:
             pattern = encoding.lower(pattern)
             matcher = lambda s: pattern in encoding.lower(s)
         else:
             matcher = lambda s: pattern in s
     return kind, pattern, matcher
 
@@ -2014,17 +2017,17 @@ def tag(repo, subset, x):
     """
     # i18n: "tag" is a keyword
     args = getargs(x, 0, 1, _("tag takes one or no arguments"))
     cl = repo.changelog
     if args:
         pattern = getstring(args[0],
                             # i18n: "tag" is a keyword
                             _('the argument to tag must be a string'))
-        kind, pattern, matcher = util.stringmatcher(pattern)
+        kind, pattern, matcher = stringutil.stringmatcher(pattern)
         if kind == 'literal':
             # avoid resolving all tags
             tn = repo._tagscache.tags.get(pattern, None)
             if tn is None:
                 raise error.RepoLookupError(_("tag '%s' does not exist")
                                             % pattern)
             s = {repo[tn].rev()}
         else:
--- a/mercurial/revsetlang.py
+++ b/mercurial/revsetlang.py
@@ -12,16 +12,19 @@ import string
 from .i18n import _
 from . import (
     error,
     node,
     parser,
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
     "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
     "[": (21, None, None, ("subscript", 1, "]"), None),
     "#": (21, None, None, ("relation", 21), None),
     "##": (20, None, None, ("_concat", 20), None),
     "~": (18, None, None, ("ancestor", 18), None),
@@ -202,17 +205,17 @@ def getinteger(x, err, default=_notset):
     if not x and default is not _notset:
         return default
     try:
         return int(getstring(x, err))
     except ValueError:
         raise error.ParseError(err)
 
 def getboolean(x, err):
-    value = util.parsebool(getsymbol(x))
+    value = stringutil.parsebool(getsymbol(x))
     if value is not None:
         return value
     raise error.ParseError(err)
 
 def getlist(x):
     if not x:
         return []
     if x[0] == 'list':
@@ -560,17 +563,17 @@ def _quote(s):
     "'asdf'"
     >>> _quote(b"asdf'\"")
     '\'asdf\\\'"\''
     >>> _quote(b'asdf\'')
     "'asdf\\''"
     >>> _quote(1)
     "'1'"
     """
-    return "'%s'" % util.escapestr(pycompat.bytestr(s))
+    return "'%s'" % stringutil.escapestr(pycompat.bytestr(s))
 
 def _formatargtype(c, arg):
     if c == 'd':
         return '%d' % int(arg)
     elif c == 's':
         return _quote(arg)
     elif c == 'r':
         parse(arg) # make sure syntax errors are confined
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -36,16 +36,20 @@ from . import (
     pycompat,
     revsetlang,
     similar,
     url,
     util,
     vfs,
 )
 
+from .utils import (
+    stringutil,
+)
+
 if pycompat.iswindows:
     from . import scmwindows as scmplatform
 else:
     from . import scmposix as scmplatform
 
 termsize = scmplatform.termsize
 
 class status(tuple):
@@ -158,22 +162,22 @@ def callcatch(ui, func):
     # Global exception handling, alphabetically
     # Mercurial-specific first, followed by built-in and library exceptions
     except error.LockHeld as inst:
         if inst.errno == errno.ETIMEDOUT:
             reason = _('timed out waiting for lock held by %r') % inst.locker
         else:
             reason = _('lock held by %r') % inst.locker
         ui.warn(_("abort: %s: %s\n")
-                % (inst.desc or util.forcebytestr(inst.filename), reason))
+                % (inst.desc or stringutil.forcebytestr(inst.filename), reason))
         if not inst.locker:
             ui.warn(_("(lock might be very busy)\n"))
     except error.LockUnavailable as inst:
         ui.warn(_("abort: could not lock %s: %s\n") %
-                (inst.desc or util.forcebytestr(inst.filename),
+                (inst.desc or stringutil.forcebytestr(inst.filename),
                  encoding.strtolocal(inst.strerror)))
     except error.OutOfBandError as inst:
         if inst.args:
             msg = _("abort: remote error:\n")
         else:
             msg = _("abort: remote error\n")
         ui.warn(msg)
         if inst.args:
@@ -189,42 +193,42 @@ def callcatch(ui, func):
         msg = inst.args[1]
         if isinstance(msg, type(u'')):
             msg = pycompat.sysbytes(msg)
         if not isinstance(msg, bytes):
             ui.warn(" %r\n" % (msg,))
         elif not msg:
             ui.warn(_(" empty string\n"))
         else:
-            ui.warn("\n%r\n" % util.ellipsis(msg))
+            ui.warn("\n%r\n" % stringutil.ellipsis(msg))
     except error.CensoredNodeError as inst:
         ui.warn(_("abort: file censored %s!\n") % inst)
     except error.RevlogError as inst:
         ui.warn(_("abort: %s!\n") % inst)
     except error.InterventionRequired as inst:
         ui.warn("%s\n" % inst)
         if inst.hint:
             ui.warn(_("(%s)\n") % inst.hint)
         return 1
     except error.WdirUnsupported:
         ui.warn(_("abort: working directory revision cannot be specified\n"))
     except error.Abort as inst:
         ui.warn(_("abort: %s\n") % inst)
         if inst.hint:
             ui.warn(_("(%s)\n") % inst.hint)
     except ImportError as inst:
-        ui.warn(_("abort: %s!\n") % util.forcebytestr(inst))
-        m = util.forcebytestr(inst).split()[-1]
+        ui.warn(_("abort: %s!\n") % stringutil.forcebytestr(inst))
+        m = stringutil.forcebytestr(inst).split()[-1]
         if m in "mpatch bdiff".split():
             ui.warn(_("(did you forget to compile extensions?)\n"))
         elif m in "zlib".split():
             ui.warn(_("(is your Python install correct?)\n"))
     except IOError as inst:
         if util.safehasattr(inst, "code"):
-            ui.warn(_("abort: %s\n") % util.forcebytestr(inst))
+            ui.warn(_("abort: %s\n") % stringutil.forcebytestr(inst))
         elif util.safehasattr(inst, "reason"):
             try: # usually it is in the form (errno, strerror)
                 reason = inst.reason.args[1]
             except (AttributeError, IndexError):
                 # it might be anything, for example a string
                 reason = inst.reason
             if isinstance(reason, unicode):
                 # SSLError of Python 2.7.9 contains a unicode
@@ -232,36 +236,36 @@ def callcatch(ui, func):
             ui.warn(_("abort: error: %s\n") % reason)
         elif (util.safehasattr(inst, "args")
               and inst.args and inst.args[0] == errno.EPIPE):
             pass
         elif getattr(inst, "strerror", None):
             if getattr(inst, "filename", None):
                 ui.warn(_("abort: %s: %s\n") % (
                     encoding.strtolocal(inst.strerror),
-                    util.forcebytestr(inst.filename)))
+                    stringutil.forcebytestr(inst.filename)))
             else:
                 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
         else:
             raise
     except OSError as inst:
         if getattr(inst, "filename", None) is not None:
             ui.warn(_("abort: %s: '%s'\n") % (
                 encoding.strtolocal(inst.strerror),
-                util.forcebytestr(inst.filename)))
+                stringutil.forcebytestr(inst.filename)))
         else:
             ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
     except MemoryError:
         ui.warn(_("abort: out of memory\n"))
     except SystemExit as inst:
         # Commands shouldn't sys.exit directly, but give a return code.
         # Just in case catch this and and pass exit code to caller.
         return inst.code
     except socket.error as inst:
-        ui.warn(_("abort: %s\n") % util.forcebytestr(inst.args[-1]))
+        ui.warn(_("abort: %s\n") % stringutil.forcebytestr(inst.args[-1]))
 
     return -1
 
 def checknewlabel(repo, lbl, kind):
     # Do not use the "kind" parameter in ui output.
     # It makes strings difficult to translate.
     if lbl in ['tip', '.', 'null']:
         raise error.Abort(_("the name '%s' is reserved") % lbl)
@@ -294,17 +298,17 @@ def checkportable(ui, f):
                 raise error.Abort(msg)
             ui.warn(_("warning: %s\n") % msg)
 
 def checkportabilityalert(ui):
     '''check if the user's config requests nothing, a warning, or abort for
     non-portable filenames'''
     val = ui.config('ui', 'portablefilenames')
     lval = val.lower()
-    bval = util.parsebool(val)
+    bval = stringutil.parsebool(val)
     abort = pycompat.iswindows or lval == 'abort'
     warn = bval or lval == 'warn'
     if bval is None and not (warn or abort or lval == 'ignore'):
         raise error.ConfigError(
             _("ui.portablefilenames value is invalid ('%s')") % val)
     return abort, warn
 
 class casecollisionauditor(object):
--- a/mercurial/simplemerge.py
+++ b/mercurial/simplemerge.py
@@ -18,17 +18,19 @@
 
 from __future__ import absolute_import
 
 from .i18n import _
 from . import (
     error,
     mdiff,
     pycompat,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 class CantReprocessAndShowBase(Exception):
     pass
 
 def intersect(ra, rb):
     """Given two ranges return the range where they intersect or None.
 
@@ -392,17 +394,17 @@ class Merge3Text(object):
             else:
                 del bm[0]
 
         return unc
 
 def _verifytext(text, path, ui, opts):
     """verifies that text is non-binary (unless opts[text] is passed,
     then we just warn)"""
-    if util.binary(text):
+    if stringutil.binary(text):
         msg = _("%s looks like a binary file.") % path
         if not opts.get('quiet'):
             ui.warn(_('warning: %s\n') % msg)
         if not opts.get('text'):
             raise error.Abort(msg)
     return text
 
 def _picklabels(defaults, overrides):
--- a/mercurial/sslutil.py
+++ b/mercurial/sslutil.py
@@ -16,16 +16,19 @@ import ssl
 
 from .i18n import _
 from . import (
     error,
     node,
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
 # all exposed via the "ssl" module.
 #
 # Depending on the version of Python being used, SSL/TLS support is either
 # modern/secure or legacy/insecure. Many operations in this module have
 # separate code paths depending on support in Python.
@@ -369,17 +372,18 @@ def wrapsocket(sock, keyfile, certfile, 
     # This still works on our fake SSLContext.
     sslcontext.verify_mode = settings['verifymode']
 
     if settings['ciphers']:
         try:
             sslcontext.set_ciphers(pycompat.sysstr(settings['ciphers']))
         except ssl.SSLError as e:
             raise error.Abort(
-                _('could not set ciphers: %s') % util.forcebytestr(e.args[0]),
+                _('could not set ciphers: %s')
+                % stringutil.forcebytestr(e.args[0]),
                 hint=_('change cipher string (%s) in config') %
                 settings['ciphers'])
 
     if certfile is not None:
         def password():
             f = keyfile or certfile
             return ui.getpass(_('passphrase for %s: ') % f, '')
         sslcontext.load_cert_chain(certfile, keyfile, password)
@@ -388,17 +392,17 @@ def wrapsocket(sock, keyfile, certfile, 
         try:
             sslcontext.load_verify_locations(cafile=settings['cafile'])
         except ssl.SSLError as e:
             if len(e.args) == 1: # pypy has different SSLError args
                 msg = e.args[0]
             else:
                 msg = e.args[1]
             raise error.Abort(_('error loading CA file %s: %s') % (
-                              settings['cafile'], util.forcebytestr(msg)),
+                              settings['cafile'], stringutil.forcebytestr(msg)),
                               hint=_('file is empty or malformed?'))
         caloaded = True
     elif settings['allowloaddefaultcerts']:
         # This is a no-op on old Python.
         sslcontext.load_default_certs()
         caloaded = True
     else:
         caloaded = False
@@ -637,17 +641,17 @@ def _verifycert(cert, hostname):
     dnsnames = []
     san = cert.get('subjectAltName', [])
     for key, value in san:
         if key == 'DNS':
             try:
                 if _dnsnamematch(value, hostname):
                     return
             except wildcarderror as e:
-                return util.forcebytestr(e.args[0])
+                return stringutil.forcebytestr(e.args[0])
 
             dnsnames.append(value)
 
     if not dnsnames:
         # The subject is only checked when there is no DNS in subjectAltName.
         for sub in cert.get(r'subject', []):
             for key, value in sub:
                 # According to RFC 2818 the most specific Common Name must
@@ -658,17 +662,17 @@ def _verifycert(cert, hostname):
                         value = value.encode('ascii')
                     except UnicodeEncodeError:
                         return _('IDN in certificate not supported')
 
                     try:
                         if _dnsnamematch(value, hostname):
                             return
                     except wildcarderror as e:
-                        return util.forcebytestr(e.args[0])
+                        return stringutil.forcebytestr(e.args[0])
 
                     dnsnames.append(value)
 
     if len(dnsnames) > 1:
         return _('certificate is for %s') % ', '.join(dnsnames)
     elif len(dnsnames) == 1:
         return _('certificate is for %s') % dnsnames[0]
     else:
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -31,17 +31,20 @@ from . import (
     pathutil,
     phases,
     pycompat,
     scmutil,
     subrepoutil,
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 hg = None
 reporelpath = subrepoutil.reporelpath
 subrelpath = subrepoutil.subrelpath
 _abssource = subrepoutil._abssource
 propertycache = util.propertycache
 
 def _expandedabspath(path):
@@ -69,17 +72,17 @@ def annotatesubrepoerror(func):
     def decoratedmethod(self, *args, **kargs):
         try:
             res = func(self, *args, **kargs)
         except SubrepoAbort as ex:
             # This exception has already been handled
             raise ex
         except error.Abort as ex:
             subrepo = subrelpath(self)
-            errormsg = (util.forcebytestr(ex) + ' '
+            errormsg = (stringutil.forcebytestr(ex) + ' '
                         + _('(in subrepository "%s")') % subrepo)
             # avoid handling this exception by raising a SubrepoAbort exception
             raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
                                cause=sys.exc_info())
         return res
     return decoratedmethod
 
 def _updateprompt(ui, sub, dirty, local, remote):
--- a/mercurial/subrepoutil.py
+++ b/mercurial/subrepoutil.py
@@ -16,16 +16,19 @@ from .i18n import _
 from . import (
     config,
     error,
     filemerge,
     pathutil,
     phases,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 nullstate = ('', '', 'empty')
 
 def state(ctx, ui):
     """return a state dict, mapping subrepo paths configured in .hgsub
     to tuple: (source from .hgsub, revision from .hgsubstate, kind
     (key in types dict))
     """
@@ -69,17 +72,17 @@ def state(ctx, ui):
         except IOError as err:
             if err.errno != errno.ENOENT:
                 raise
 
     def remap(src):
         for pattern, repl in p.items('subpaths'):
             # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
             # does a string decode.
-            repl = util.escapestr(repl)
+            repl = stringutil.escapestr(repl)
             # However, we still want to allow back references to go
             # through unharmed, so we turn r'\\1' into r'\1'. Again,
             # extra escapes are needed because re.sub string decodes.
             repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
             try:
                 src = re.sub(pattern, repl, src, 1)
             except re.error as e:
                 raise error.Abort(_("bad subrepository pattern in %s: %s")
--- a/mercurial/tags.py
+++ b/mercurial/tags.py
@@ -23,16 +23,19 @@ from .node import (
 from .i18n import _
 from . import (
     encoding,
     error,
     match as matchmod,
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 # Tags computation can be expensive and caches exist to make it fast in
 # the common case.
 #
 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
 # each revision in the repository. The file is effectively an array of
 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
 # details.
@@ -778,11 +781,11 @@ class hgtagsfnodescache(object):
                             len(data), _fnodescachefile))
                 f.write(data)
                 self._dirtyoffset = None
             finally:
                 f.close()
         except (IOError, OSError) as inst:
             repo.ui.log('tagscache',
                         "couldn't write cache/%s: %s\n" % (
-                        _fnodescachefile, util.forcebytestr(inst)))
+                            _fnodescachefile, stringutil.forcebytestr(inst)))
         finally:
             lock.release()
--- a/mercurial/templatefilters.py
+++ b/mercurial/templatefilters.py
@@ -16,17 +16,20 @@ from . import (
     error,
     node,
     pycompat,
     registrar,
     templateutil,
     url,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 if pycompat.ispy3:
     long = int
 
 # filters are callables like:
@@ -123,17 +126,17 @@ def domain(author):
     return author
 
 @templatefilter('email')
 def email(text):
     """Any text. Extracts the first string that looks like an email
     address. Example: ``User <user@example.com>`` becomes
     ``user@example.com``.
     """
-    return util.email(text)
+    return stringutil.email(text)
 
 @templatefilter('escape')
 def escape(text):
     """Any text. Replaces the special XML/XHTML characters "&", "<"
     and ">" with XML entities, and filters out NUL characters.
     """
     return url.escape(text.replace('\0', ''), True)
 
@@ -157,18 +160,19 @@ def fill(text, width, initindent='', han
                 while 0 < w and uctext[w - 1].isspace():
                     w -= 1
                 yield (encoding.unitolocal(uctext[:w]),
                        encoding.unitolocal(uctext[w:]))
                 break
             yield text[start:m.start(0)], m.group(1)
             start = m.end(1)
 
-    return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
-                              width, initindent, hangindent) + rest
+    return "".join([stringutil.wrap(space_re.sub(' ',
+                                                 stringutil.wrap(para, width)),
+                                    width, initindent, hangindent) + rest
                     for para, rest in findparas()])
 
 @templatefilter('fill68')
 def fill68(text):
     """Any text. Wraps the text to fit in 68 columns."""
     return fill(text, 68)
 
 @templatefilter('fill76')
@@ -364,17 +368,17 @@ def slashpath(path):
 
 @templatefilter('splitlines')
 def splitlines(text):
     """Any text. Split text into a list of lines."""
     return templateutil.hybridlist(text.splitlines(), name='line')
 
 @templatefilter('stringescape')
 def stringescape(text):
-    return util.escapestr(text)
+    return stringutil.escapestr(text)
 
 @templatefilter('stringify')
 def stringify(thing):
     """Any type. Turns the value into text by converting values into
     text and concatenating them.
     """
     return templateutil.stringify(thing)
 
@@ -407,22 +411,22 @@ def urlescape(text):
     "foo bar" becomes "foo%20bar".
     """
     return urlreq.quote(text)
 
 @templatefilter('user')
 def userfilter(text):
     """Any text. Returns a short representation of a user name or email
     address."""
-    return util.shortuser(text)
+    return stringutil.shortuser(text)
 
 @templatefilter('emailuser')
 def emailuser(text):
     """Any text. Returns the user portion of an email address."""
-    return util.emailuser(text)
+    return stringutil.emailuser(text)
 
 @templatefilter('utf8')
 def utf8(text):
     """Any text. Converts from the local character encoding to UTF-8."""
     return encoding.fromlocal(text)
 
 @templatefilter('xmlescape')
 def xmlescape(text):
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -21,16 +21,19 @@ from . import (
     obsutil,
     patch,
     pycompat,
     registrar,
     scmutil,
     templateutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 _hybrid = templateutil.hybrid
 _mappable = templateutil.mappable
 hybriddict = templateutil.hybriddict
 hybridlist = templateutil.hybridlist
 compatdict = templateutil.compatdict
 compatlist = templateutil.compatlist
 _showcompatlist = templateutil._showcompatlist
@@ -67,17 +70,17 @@ def getlatesttags(context, mapping, patt
     '''return date, distance and name for the latest tag of rev'''
     repo = context.resource(mapping, 'repo')
     ctx = context.resource(mapping, 'ctx')
     cache = context.resource(mapping, 'cache')
 
     cachename = 'latesttags'
     if pattern is not None:
         cachename += '-' + pattern
-        match = util.stringmatcher(pattern)[2]
+        match = stringutil.stringmatcher(pattern)[2]
     else:
         match = util.always
 
     if cachename not in cache:
         # Cache mapping from rev to a tuple with tag date, tag
         # distance and tag name
         cache[cachename] = {-1: (0, 0, ['null'])}
     latesttags = cache[cachename]
@@ -302,17 +305,17 @@ def showextras(context, mapping):
     field of this changeset."""
     ctx = context.resource(mapping, 'ctx')
     extras = ctx.extra()
     extras = util.sortdict((k, extras[k]) for k in sorted(extras))
     makemap = lambda k: {'key': k, 'value': extras[k]}
     c = [makemap(k) for k in extras]
     f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
     return _hybrid(f, extras, makemap,
-                   lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
+                   lambda k: '%s=%s' % (k, stringutil.escapestr(extras[k])))
 
 def _showfilesbystat(context, mapping, name, index):
     repo = context.resource(mapping, 'repo')
     ctx = context.resource(mapping, 'ctx')
     revcache = context.resource(mapping, 'revcache')
     if 'files' not in revcache:
         revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
     files = revcache['files'][index]
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -58,16 +58,19 @@ from . import (
     error,
     parser,
     pycompat,
     templatefilters,
     templatefuncs,
     templateutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 # template parsing
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
     "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
     ".": (18, None, None, (".", 18), None),
     "%": (15, None, None, ("%", 15), None),
@@ -806,17 +809,18 @@ class templater(object):
         if t not in self.cache:
             try:
                 self.cache[t] = util.readfile(self.map[t][1])
             except KeyError as inst:
                 raise templateutil.TemplateNotFound(
                     _('"%s" not in template map') % inst.args[0])
             except IOError as inst:
                 reason = (_('template file %s: %s')
-                          % (self.map[t][1], util.forcebytestr(inst.args[1])))
+                          % (self.map[t][1],
+                             stringutil.forcebytestr(inst.args[1])))
                 raise IOError(inst.args[0], encoding.strfromlocal(reason))
         return self.cache[t]
 
     def renderdefault(self, mapping):
         """Render the default unnamed template and return result as string"""
         return self.render('', mapping)
 
     def render(self, t, mapping):
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -10,16 +10,19 @@ from __future__ import absolute_import
 import types
 
 from .i18n import _
 from . import (
     error,
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class ResourceUnavailable(error.Abort):
     pass
 
 class TemplateNotFound(error.Abort):
     pass
 
 class hybrid(object):
@@ -276,17 +279,17 @@ def evalfuncarg(context, mapping, arg):
 
 def evalboolean(context, mapping, arg):
     """Evaluate given argument as boolean, but also takes boolean literals"""
     func, data = arg
     if func is runsymbol:
         thing = func(context, mapping, data, default=None)
         if thing is None:
             # not a template keyword, takes as a boolean literal
-            thing = util.parsebool(data)
+            thing = stringutil.parsebool(data)
     else:
         thing = func(context, mapping, data)
     thing = unwrapvalue(thing)
     if isinstance(thing, bool):
         return thing
     # other objects are evaluated as strings, which means 0 is True, but
     # empty dict/list should be False as they are expected to be ''
     return bool(stringify(thing))
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -32,17 +32,20 @@ from . import (
     error,
     formatter,
     progress,
     pycompat,
     rcutil,
     scmutil,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 urlreq = util.urlreq
 
 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
                      if not c.isalnum())
 
 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
@@ -366,17 +369,17 @@ class ui(object):
         trusted = sections or trust or self._trusted(fp, filename)
 
         try:
             cfg.read(filename, fp, sections=sections, remap=remap)
             fp.close()
         except error.ConfigError as inst:
             if trusted:
                 raise
-            self.warn(_("ignored: %s\n") % util.forcebytestr(inst))
+            self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
 
         if self.plain():
             for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
                       'logtemplate', 'statuscopies', 'style',
                       'traceback', 'verbose'):
                 if k in cfg['ui']:
                     del cfg['ui'][k]
             for k, v in cfg.items('defaults'):
@@ -586,17 +589,17 @@ class ui(object):
         if v is None:
             return v
         if v is _unset:
             if default is _unset:
                 return False
             return default
         if isinstance(v, bool):
             return v
-        b = util.parsebool(v)
+        b = stringutil.parsebool(v)
         if b is None:
             raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
                                     % (section, name, v))
         return b
 
     def configwith(self, convert, section, name, default=_unset,
                    desc=None, untrusted=False):
         """parse a configuration element with a conversion function
@@ -816,17 +819,17 @@ class ui(object):
         if "\n" in user:
             raise error.Abort(_("username %r contains a newline\n")
                               % pycompat.bytestr(user))
         return user
 
     def shortuser(self, user):
         """Return a short representation of a user name or email address."""
         if not self.verbose:
-            user = util.shortuser(user)
+            user = stringutil.shortuser(user)
         return user
 
     def expandpath(self, loc, default=None):
         """Return repository location relative to cwd or from [paths]"""
         try:
             p = self.paths.getpath(loc)
             if p:
                 return p.rawloc
--- a/mercurial/url.py
+++ b/mercurial/url.py
@@ -19,16 +19,19 @@ from . import (
     error,
     httpconnection as httpconnectionmod,
     keepalive,
     pycompat,
     sslutil,
     urllibcompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 httplib = util.httplib
 stringio = util.stringio
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 def escape(s, quote=None):
     '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
@@ -472,17 +475,17 @@ class cookiehandler(urlreq.basehandler):
 
         cookiefile = util.expandpath(cookiefile)
         try:
             cookiejar = util.cookielib.MozillaCookieJar(cookiefile)
             cookiejar.load()
             self.cookiejar = cookiejar
         except util.cookielib.LoadError as e:
             ui.warn(_('(error loading cookie file %s: %s; continuing without '
-                      'cookies)\n') % (cookiefile, util.forcebytestr(e)))
+                      'cookies)\n') % (cookiefile, stringutil.forcebytestr(e)))
 
     def http_request(self, request):
         if self.cookiejar:
             self.cookiejar.add_cookie_header(request)
 
         return request
 
     def https_request(self, request):
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -815,29 +815,31 @@ class baseproxyobserver(object):
             if self.logdataapis:
                 self.fh.write('\n')
                 self.fh.flush()
             return
 
         # Simple case writes all data on a single line.
         if b'\n' not in data:
             if self.logdataapis:
-                self.fh.write(': %s\n' % escapedata(data))
+                self.fh.write(': %s\n' % stringutil.escapedata(data))
             else:
-                self.fh.write('%s>     %s\n' % (self.name, escapedata(data)))
+                self.fh.write('%s>     %s\n'
+                              % (self.name, stringutil.escapedata(data)))
             self.fh.flush()
             return
 
         # Data with newlines is written to multiple lines.
         if self.logdataapis:
             self.fh.write(':\n')
 
         lines = data.splitlines(True)
         for line in lines:
-            self.fh.write('%s>     %s\n' % (self.name, escapedata(line)))
+            self.fh.write('%s>     %s\n'
+                          % (self.name, stringutil.escapedata(line)))
         self.fh.flush()
 
 class fileobjectobserver(baseproxyobserver):
     """Logs file object activity."""
     def __init__(self, fh, name, reads=True, writes=True, logdata=False,
                  logdataapis=True):
         self.fh = fh
         self.name = name
@@ -1910,17 +1912,17 @@ def checkwinfilename(path):
         if not n:
             continue
         for c in _filenamebytestr(n):
             if c in _winreservedchars:
                 return _("filename contains '%s', which is reserved "
                          "on Windows") % c
             if ord(c) <= 31:
                 return _("filename contains '%s', which is invalid "
-                         "on Windows") % escapestr(c)
+                         "on Windows") % stringutil.escapestr(c)
         base = n.split('.')[0]
         if base and base.lower() in _winreservednames:
             return _("filename contains '%s', which is reserved "
                      "on Windows") % base
         t = n[-1:]
         if t in '. ' and n not in '..':
             return _("filename ends with '%s', which is not allowed "
                      "on Windows") % t
@@ -3674,17 +3676,17 @@ class _zlibengine(compressionengine):
                     return ''.join(parts)
                 return None
 
         def decompress(self, data):
             try:
                 return zlib.decompress(data)
             except zlib.error as e:
                 raise error.RevlogError(_('revlog decompress error: %s') %
-                                        forcebytestr(e))
+                                        stringutil.forcebytestr(e))
 
     def revlogcompressor(self, opts=None):
         return self.zlibrevlogcompressor()
 
 compengines.register(_zlibengine())
 
 class _bz2engine(compressionengine):
     def name(self):
@@ -3900,17 +3902,17 @@ class _zstdengine(compressionengine):
                     if chunk:
                         chunks.append(chunk)
                     pos = pos2
                 # Frame should be exhausted, so no finish() API.
 
                 return ''.join(chunks)
             except Exception as e:
                 raise error.RevlogError(_('revlog decompress error: %s') %
-                                        forcebytestr(e))
+                                        stringutil.forcebytestr(e))
 
     def revlogcompressor(self, opts=None):
         opts = opts or {}
         return self.zstdrevlogcompressor(self._module,
                                          level=opts.get('level', 3))
 
 compengines.register(_zstdengine())
 
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -29,16 +29,20 @@ from . import (
     pushkey as pushkeymod,
     pycompat,
     repository,
     streamclone,
     util,
     wireprototypes,
 )
 
+from .utils import (
+    stringutil,
+)
+
 urlerr = util.urlerr
 urlreq = util.urlreq
 
 bytesresponse = wireprototypes.bytesresponse
 ooberror = wireprototypes.ooberror
 pushres = wireprototypes.pushres
 pusherr = wireprototypes.pusherr
 streamres = wireprototypes.streamres
@@ -989,30 +993,30 @@ def listkeys(repo, proto, namespace):
 @wireprotocommand('lookup', 'key', permission='pull')
 def lookup(repo, proto, key):
     try:
         k = encoding.tolocal(key)
         c = repo[k]
         r = c.hex()
         success = 1
     except Exception as inst:
-        r = util.forcebytestr(inst)
+        r = stringutil.forcebytestr(inst)
         success = 0
     return bytesresponse('%d %s\n' % (success, r))
 
 @wireprotocommand('known', 'nodes *', permission='pull')
 def known(repo, proto, nodes, others):
     v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
     return bytesresponse(v)
 
 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
 def pushkey(repo, proto, namespace, key, old, new):
     # compatibility with pre-1.8 clients which were accidentally
     # sending raw binary nodes rather than utf-8-encoded hex
-    if len(new) == 20 and util.escapestr(new) != new:
+    if len(new) == 20 and stringutil.escapestr(new) != new:
         # looks like it could be a binary node
         try:
             new.decode('utf-8')
             new = encoding.tolocal(new) # but cleanly decodes as UTF-8
         except UnicodeDecodeError:
             pass # binary, leave unmodified
     else:
         new = encoding.tolocal(new) # normal path
@@ -1118,18 +1122,18 @@ def unbundle(repo, proto, heads):
                         part.addparam('ret', exc.ret, mandatory=False)
             except error.BundleValueError as exc:
                 errpart = bundler.newpart('error:unsupportedcontent')
                 if exc.parttype is not None:
                     errpart.addparam('parttype', exc.parttype)
                 if exc.params:
                     errpart.addparam('params', '\0'.join(exc.params))
             except error.Abort as exc:
-                manargs = [('message', util.forcebytestr(exc))]
+                manargs = [('message', stringutil.forcebytestr(exc))]
                 advargs = []
                 if exc.hint is not None:
                     advargs.append(('hint', exc.hint))
                 bundler.addpart(bundle2.bundlepart('error:abort',
                                                    manargs, advargs))
             except error.PushRaced as exc:
                 bundler.newpart('error:pushraced',
-                                [('message', util.forcebytestr(exc))])
+                                [('message', stringutil.forcebytestr(exc))])
             return streamres_legacy(gen=bundler.getchunks())
--- a/mercurial/wireprotoframing.py
+++ b/mercurial/wireprotoframing.py
@@ -16,16 +16,19 @@ import struct
 from .i18n import _
 from .thirdparty import (
     attr,
 )
 from . import (
     error,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 FRAME_HEADER_SIZE = 6
 DEFAULT_MAX_FRAME_SIZE = 32768
 
 FRAME_TYPE_COMMAND_NAME = 0x01
 FRAME_TYPE_COMMAND_ARGUMENT = 0x02
 FRAME_TYPE_COMMAND_DATA = 0x03
 FRAME_TYPE_BYTES_RESPONSE = 0x04
@@ -159,17 +162,17 @@ def makeframefromhumanstring(s):
     finalflags = 0
     validflags = FRAME_TYPE_FLAGS[frametype]
     for flag in frameflags.split(b'|'):
         if flag in validflags:
             finalflags |= validflags[flag]
         else:
             finalflags |= int(flag)
 
-    payload = util.unescapestr(payload)
+    payload = stringutil.unescapestr(payload)
 
     return makeframe(requestid=requestid, typeid=frametype,
                      flags=finalflags, payload=payload)
 
 def parseheader(data):
     """Parse a unified framing protocol frame header from a buffer.
 
     The header is expected to be in the buffer at offset 0 and the
--- a/tests/test-simplemerge.py
+++ b/tests/test-simplemerge.py
@@ -17,29 +17,34 @@ from __future__ import absolute_import
 
 import unittest
 from mercurial import (
     error,
     simplemerge,
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 TestCase = unittest.TestCase
 # bzr compatible interface, for the tests
 class Merge3(simplemerge.Merge3Text):
     """3-way merge of texts.
 
     Given BASE, OTHER, THIS, tries to produce a combined text
     incorporating the changes from both BASE->OTHER and BASE->THIS.
     All three will typically be sequences of lines."""
     def __init__(self, base, a, b):
         basetext = '\n'.join([i.strip('\n') for i in base] + [''])
         atext = '\n'.join([i.strip('\n') for i in a] + [''])
         btext = '\n'.join([i.strip('\n') for i in b] + [''])
-        if util.binary(basetext) or util.binary(atext) or util.binary(btext):
+        if (stringutil.binary(basetext) or stringutil.binary(atext)
+            or stringutil.binary(btext)):
             raise error.Abort("don't know how to merge binary files")
         simplemerge.Merge3Text.__init__(self, basetext, atext, btext,
                                         base, a, b)
 
 CantReprocessAndShowBase = simplemerge.CantReprocessAndShowBase
 
 def split_lines(t):
     return util.stringio(t).readlines()
@@ -353,9 +358,8 @@ if __name__ == '__main__':
     # hide the timer
     import time
     orig = time.time
     try:
         time.time = lambda: 0
         unittest.main()
     finally:
         time.time = orig
-