Patchwork [7,of,8] stringutil: bulk-replace call sites to point to new module

login
register
mail settings
Submitter Yuya Nishihara
Date March 22, 2018, 3:01 p.m.
Message ID <7b0c9a92447bf080befe.1521730918@mimosa>
Download mbox | patch
Permalink /patch/29787/
State Accepted
Headers show

Comments

Yuya Nishihara - March 22, 2018, 3:01 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1521723380 -32400
#      Thu Mar 22 21:56:20 2018 +0900
# Node ID 7b0c9a92447bf080befe051566fcc8228d957cc0
# Parent  882b04a9062461a23585f3c36fa1440533addb28
stringutil: bulk-replace call sites to point to new module

This might conflict with other patches floating around, sorry.

Patch

diff --git a/hgext/bugzilla.py b/hgext/bugzilla.py
--- a/hgext/bugzilla.py
+++ b/hgext/bugzilla.py
@@ -307,6 +307,9 @@  from mercurial import (
     url,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 xmlrpclib = util.xmlrpclib
 
@@ -1099,7 +1102,8 @@  class bugzilla(object):
                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.'''
@@ -1119,6 +1123,6 @@  def hook(ui, repo, hooktype, node=None, 
         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)
diff --git a/hgext/convert/cvsps.py b/hgext/convert/cvsps.py
--- a/hgext/convert/cvsps.py
+++ b/hgext/convert/cvsps.py
@@ -17,7 +17,10 @@  from mercurial import (
     pycompat,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 pickle = util.pickle
 
@@ -452,7 +455,8 @@  def createlog(ui, directory=None, root="
             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))
 
@@ -608,7 +612,7 @@  def createchangeset(ui, log, fuzz=60, me
             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)
diff --git a/hgext/convert/p4.py b/hgext/convert/p4.py
--- a/hgext/convert/p4.py
+++ b/hgext/convert/p4.py
@@ -14,7 +14,10 @@  from mercurial import (
     error,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 from . import common
 
@@ -169,7 +172,7 @@  class p4_source(common.converter_source)
                 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 = {}
diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py
--- a/hgext/convert/subversion.py
+++ b/hgext/convert/subversion.py
@@ -16,7 +16,10 @@  from mercurial import (
     util,
     vfs as vfsmod,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 from . import common
 
@@ -147,7 +150,7 @@  def get_log_child(fp, url, paths, start,
         # 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()
@@ -1315,7 +1318,7 @@  class svn_sink(converter_sink, commandli
         fp.close()
         try:
             output = self.run0('commit',
-                               username=util.shortuser(commit.author),
+                               username=stringutil.shortuser(commit.author),
                                file=messagefile,
                                encoding='utf-8')
             try:
diff --git a/hgext/eol.py b/hgext/eol.py
--- a/hgext/eol.py
+++ b/hgext/eol.py
@@ -105,6 +105,9 @@  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
@@ -133,7 +136,7 @@  def inconsistenteol(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
@@ -144,7 +147,7 @@  def tolf(s, params, ui, **kwargs):
 
 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
@@ -403,7 +406,7 @@  def reposetup(ui, repo):
                 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.
diff --git a/hgext/extdiff.py b/hgext/extdiff.py
--- a/hgext/extdiff.py
+++ b/hgext/extdiff.py
@@ -82,6 +82,9 @@  from mercurial import (
     scmutil,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -367,8 +370,8 @@  class savedcmd(object):
     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):
diff --git a/hgext/highlight/highlight.py b/hgext/highlight/highlight.py
--- a/hgext/highlight/highlight.py
+++ b/hgext/highlight/highlight.py
@@ -15,7 +15,10 @@  demandimport.ignore.extend(['pkgutil', '
 
 from mercurial import (
     encoding,
-    util,
+)
+
+from mercurial.utils import (
+    stringutil,
 )
 
 with demandimport.deactivated():
@@ -47,7 +50,7 @@  def pygmentize(field, fctx, style, tmpl,
         tmpl.cache['header'] = new_header
 
     text = fctx.data()
-    if util.binary(text):
+    if stringutil.binary(text):
         return
 
     # str.splitlines() != unicode.splitlines() because "reasons"
diff --git a/hgext/histedit.py b/hgext/histedit.py
--- a/hgext/histedit.py
+++ b/hgext/histedit.py
@@ -209,6 +209,9 @@  from mercurial import (
     scmutil,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 pickle = util.pickle
 release = lock.release
@@ -465,7 +468,7 @@  class histeditaction(object):
         # (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
diff --git a/hgext/journal.py b/hgext/journal.py
--- a/hgext/journal.py
+++ b/hgext/journal.py
@@ -36,7 +36,10 @@  from mercurial import (
     registrar,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -376,9 +379,9 @@  class journalstorage(object):
 
         """
         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
diff --git a/hgext/keyword.py b/hgext/keyword.py
--- a/hgext/keyword.py
+++ b/hgext/keyword.py
@@ -111,7 +111,10 @@  from mercurial import (
     templatefilters,
     util,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -272,7 +275,8 @@  class kwtemplater(object):
 
     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
@@ -304,7 +308,7 @@  class kwtemplater(object):
                 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()
@@ -335,7 +339,7 @@  class kwtemplater(object):
 
     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
 
@@ -343,7 +347,7 @@  class kwtemplater(object):
         '''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
 
diff --git a/hgext/largefiles/remotestore.py b/hgext/largefiles/remotestore.py
--- a/hgext/largefiles/remotestore.py
+++ b/hgext/largefiles/remotestore.py
@@ -14,6 +14,10 @@  from mercurial import (
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 from . import (
     basestore,
     lfutil,
@@ -52,7 +56,7 @@  class remotestore(basestore.basestore):
         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:
@@ -61,7 +65,7 @@  class remotestore(basestore.basestore):
             # 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
@@ -70,7 +74,7 @@  class remotestore(basestore.basestore):
                              (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)
 
diff --git a/hgext/lfs/wrapper.py b/hgext/lfs/wrapper.py
--- a/hgext/lfs/wrapper.py
+++ b/hgext/lfs/wrapper.py
@@ -19,6 +19,10 @@  from mercurial import (
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 from ..largefiles import lfutil
 
 from . import (
@@ -95,7 +99,7 @@  def writetostore(self, 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'
 
diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -98,7 +98,10 @@  from mercurial import (
     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'))]
@@ -469,7 +472,7 @@  class queue(object):
         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'
@@ -724,7 +727,7 @@  class queue(object):
             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
@@ -857,7 +860,7 @@  class queue(object):
                                   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()
@@ -1917,7 +1920,7 @@  class queue(object):
                 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)
diff --git a/hgext/narrow/narrowbundle2.py b/hgext/narrow/narrowbundle2.py
--- a/hgext/narrow/narrowbundle2.py
+++ b/hgext/narrow/narrowbundle2.py
@@ -29,6 +29,9 @@  from mercurial import (
     util,
     wireproto,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 NARROWCAP = 'narrow'
 _NARROWACL_SECTION = 'narrowhgacl'
@@ -449,7 +452,7 @@  def handlechangegroup_widen(op, inpart):
         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)
diff --git a/hgext/notify.py b/hgext/notify.py
--- a/hgext/notify.py
+++ b/hgext/notify.py
@@ -149,7 +149,10 @@  from mercurial import (
     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
@@ -277,7 +280,7 @@  class notifier(object):
     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:
@@ -372,7 +375,7 @@  class notifier(object):
                 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)
 
@@ -399,7 +402,7 @@  class notifier(object):
         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):
diff --git a/hgext/relink.py b/hgext/relink.py
--- a/hgext/relink.py
+++ b/hgext/relink.py
@@ -18,6 +18,9 @@  from mercurial import (
     registrar,
     util,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -187,7 +190,7 @@  def do_relink(src, dst, files, ui):
             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)
 
diff --git a/hgext/shelve.py b/hgext/shelve.py
--- a/hgext/shelve.py
+++ b/hgext/shelve.py
@@ -56,7 +56,10 @@  from mercurial import (
 from . import (
     rebase,
 )
-from mercurial.utils import dateutil
+from mercurial.utils import (
+    dateutil,
+    stringutil,
+)
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -477,7 +480,7 @@  def _docreatecmd(ui, repo, pats, opts):
         _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):
@@ -578,7 +581,7 @@  def listcmd(ui, repo, pats, opts):
                 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')
diff --git a/hgext/transplant.py b/hgext/transplant.py
--- a/hgext/transplant.py
+++ b/hgext/transplant.py
@@ -38,6 +38,9 @@  from mercurial import (
     util,
     vfs as vfsmod,
 )
+from mercurial.utils import (
+    stringutil,
+)
 
 class TransplantError(error.Abort):
     pass
@@ -311,7 +314,7 @@  class transplanter(object):
                 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:
diff --git a/hgext/win32text.py b/hgext/win32text.py
--- a/hgext/win32text.py
+++ b/hgext/win32text.py
@@ -50,7 +50,9 @@  from mercurial.node import (
 )
 from mercurial import (
     registrar,
-    util,
+)
+from mercurial.utils import (
+    stringutil,
 )
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
@@ -100,22 +102,22 @@  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
 
@@ -146,7 +148,7 @@  def forbidnewline(ui, repo, hooktype, no
                 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') %
diff --git a/mercurial/branchmap.py b/mercurial/branchmap.py
--- a/mercurial/branchmap.py
+++ b/mercurial/branchmap.py
@@ -22,6 +22,9 @@  from . import (
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 calcsize = struct.calcsize
 pack_into = struct.pack_into
@@ -256,7 +259,7 @@  class branchcache(dict):
         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
@@ -378,7 +381,7 @@  class revbranchcache(object):
                 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))
@@ -540,7 +543,7 @@  class revbranchcache(object):
                 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()
diff --git a/mercurial/bundle2.py b/mercurial/bundle2.py
--- a/mercurial/bundle2.py
+++ b/mercurial/bundle2.py
@@ -171,6 +171,9 @@  from . import (
     url,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -1091,7 +1094,7 @@  class bundlepart(object):
             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)
diff --git a/mercurial/changegroup.py b/mercurial/changegroup.py
--- a/mercurial/changegroup.py
+++ b/mercurial/changegroup.py
@@ -28,6 +28,10 @@  from . import (
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 _CHANGEGROUPV1_DELTA_HEADER = "20s20s20s20s"
 _CHANGEGROUPV2_DELTA_HEADER = "20s20s20s20s20s"
 _CHANGEGROUPV3_DELTA_HEADER = ">20s20s20s20s20sH"
@@ -514,7 +518,7 @@  class cg1packer(object):
         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
diff --git a/mercurial/changelog.py b/mercurial/changelog.py
--- a/mercurial/changelog.py
+++ b/mercurial/changelog.py
@@ -24,7 +24,10 @@  from . import (
     revlog,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 _defaultextra = {'branch': 'default'}
 
@@ -36,7 +39,7 @@  def _string_escape(text):
     >>> 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
@@ -62,7 +65,7 @@  def decodeextra(text):
                 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
 
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -48,7 +48,12 @@  from . import (
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+
+from .utils import (
+    dateutil,
+    stringutil,
+)
+
 stringio = util.stringio
 
 # templates of common command options
@@ -962,9 +967,9 @@  def _buildfntemplate(pat, total=None, se
         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"))
@@ -1479,7 +1484,7 @@  def tryimportone(ui, repo, hunk, parents
                     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:
diff --git a/mercurial/color.py b/mercurial/color.py
--- a/mercurial/color.py
+++ b/mercurial/color.py
@@ -14,7 +14,10 @@  from .i18n import _
 from . import (
     encoding,
     pycompat,
-    util
+)
+
+from .utils import (
+    stringutil,
 )
 
 try:
@@ -200,7 +203,7 @@  def _modesetup(ui):
 
     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':
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -61,7 +61,10 @@  from . import (
     util,
     wireprotoserver,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 release = lockmod.release
 
@@ -2469,7 +2472,7 @@  def grep(ui, repo, pattern, *pats, **opt
         @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'):
@@ -3914,7 +3917,7 @@  def postincoming(ui, repo, modheads, opt
         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:
diff --git a/mercurial/context.py b/mercurial/context.py
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -46,7 +46,10 @@  from . import (
     subrepoutil,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 propertycache = util.propertycache
 
@@ -818,7 +821,7 @@  class basefilectx(object):
 
     def isbinary(self):
         try:
-            return util.binary(self.data())
+            return stringutil.binary(self.data())
         except IOError:
             return False
     def isexec(self):
@@ -1500,7 +1503,8 @@  class workingctx(committablectx):
         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
diff --git a/mercurial/crecord.py b/mercurial/crecord.py
--- a/mercurial/crecord.py
+++ b/mercurial/crecord.py
@@ -23,6 +23,9 @@  from . import (
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 stringio = util.stringio
 
 # This is required for ncurses to display non-ASCII characters in default user
@@ -585,7 +588,7 @@  class curseschunkselector(object):
         # 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
@@ -1058,7 +1061,7 @@  class curseschunkselector(object):
         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()
diff --git a/mercurial/dagparser.py b/mercurial/dagparser.py
--- a/mercurial/dagparser.py
+++ b/mercurial/dagparser.py
@@ -14,7 +14,9 @@  from .i18n import _
 from . import (
     error,
     pycompat,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 def parsedag(desc):
@@ -372,8 +374,8 @@  def dagtextlines(events,
                 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
 
diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py
--- a/mercurial/debugcommands.py
+++ b/mercurial/debugcommands.py
@@ -81,7 +81,10 @@  from . import (
     wireprotoframing,
     wireprotoserver,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 release = lockmod.release
 
@@ -1141,7 +1144,7 @@  def debuginstall(ui, **opts):
     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)
@@ -1197,7 +1200,7 @@  def debuginstall(ui, **opts):
             )
             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)
 
@@ -1234,7 +1237,7 @@  def debuginstall(ui, **opts):
             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:
@@ -1271,7 +1274,7 @@  def debuginstall(ui, **opts):
     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)
@@ -1822,8 +1825,8 @@  def debugpushkey(ui, repopath, namespace
         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):
@@ -2909,7 +2912,7 @@  def debugwireproto(ui, repo, path=None, 
 
             # 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+':
@@ -2935,7 +2938,7 @@  def debugwireproto(ui, repo, path=None, 
                 else:
                     key, value = fields
 
-                args[key] = util.unescapestr(value)
+                args[key] = stringutil.unescapestr(value)
 
             if batchedcommands is not None:
                 batchedcommands.append((command, args))
@@ -2948,12 +2951,12 @@  def debugwireproto(ui, repo, path=None, 
                     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:
@@ -2967,7 +2970,8 @@  def debugwireproto(ui, repo, path=None, 
             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
 
diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -41,6 +41,10 @@  from . import (
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 unrecoverablewrite = registrar.command.unrecoverablewrite
 
 class request(object):
@@ -496,7 +500,7 @@  class cmdalias(object):
             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:
@@ -623,7 +627,7 @@  def _parse(ui, args):
     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:]
@@ -647,7 +651,7 @@  def _parse(ui, args):
     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:
@@ -872,7 +876,7 @@  def _dispatch(req):
                 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':
@@ -968,7 +972,7 @@  def _exceptionwarning(ui):
         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.
@@ -990,7 +994,8 @@  def _exceptionwarning(ui):
     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'
diff --git a/mercurial/exchange.py b/mercurial/exchange.py
--- a/mercurial/exchange.py
+++ b/mercurial/exchange.py
@@ -35,6 +35,9 @@  from . import (
     url as urlmod,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -2180,7 +2183,7 @@  def filterclonebundleentries(repo, entri
             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.
@@ -2286,9 +2289,9 @@  def trypullbundlefromurl(ui, repo, 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
diff --git a/mercurial/extensions.py b/mercurial/extensions.py
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -25,6 +25,10 @@  from . import (
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 _extensions = {}
 _disabledextensions = {}
 _aftercallbacks = {}
@@ -118,7 +122,7 @@  def _reportimporterror(ui, err, failed, 
     # 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()
 
@@ -129,7 +133,7 @@  def _rejectunicode(name, xs):
     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")
@@ -198,7 +202,7 @@  def _runuisetup(name, ui):
             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
@@ -215,7 +219,7 @@  def _runextsetup(name, ui):
                 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
@@ -233,7 +237,7 @@  def loadall(ui, whitelist=None):
         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))
diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -31,6 +31,10 @@  from . import (
     util,
 )
 
+from .utils import (
+    stringutil,
+)
+
 def _toolstr(ui, tool, part, *args):
     return ui.config("merge-tools", tool + "." + part, *args)
 
@@ -573,7 +577,7 @@  def _formatconflictmarker(ctx, template,
         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']
 
diff --git a/mercurial/fileset.py b/mercurial/fileset.py
--- a/mercurial/fileset.py
+++ b/mercurial/fileset.py
@@ -20,6 +20,9 @@  from . import (
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
@@ -445,7 +448,7 @@  def eol(mctx, x):
     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)
diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -48,6 +48,10 @@  from . import (
     vfs as vfsmod,
 )
 
+from .utils import (
+    stringutil,
+)
+
 release = lock.release
 
 # shared features
@@ -270,7 +274,7 @@  def share(ui, source, dest=None, update=
             # 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'
 
diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py
+++ b/mercurial/hgweb/webcommands.py
@@ -36,7 +36,10 @@  from .. import (
     scmutil,
     smartset,
     templater,
-    util,
+)
+
+from ..utils import (
+    stringutil,
 )
 
 from . import (
@@ -121,7 +124,7 @@  def rawfile(web):
     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'
@@ -141,7 +144,7 @@  def _filerevision(web, fctx):
     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
 
diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -38,6 +38,10 @@  from .. import (
     util,
 )
 
+from ..utils import (
+    stringutil,
+)
+
 def up(p):
     if p[0:1] != "/":
         p = "/" + p
@@ -180,7 +184,7 @@  def difffeatureopts(req, ui, section):
     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
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -64,6 +64,9 @@  from . import (
     util,
     vfs as vfsmod,
 )
+from .utils import (
+    stringutil,
+)
 
 release = lockmod.release
 urlerr = util.urlerr
@@ -263,7 +266,7 @@  class localpeer(repository.peer):
                 raise
         except error.PushRaced as exc:
             raise error.ResponseError(_('push failed:'),
-                                      util.forcebytestr(exc))
+                                      stringutil.forcebytestr(exc))
 
     # End of _basewirecommands interface.
 
diff --git a/mercurial/logcmdutil.py b/mercurial/logcmdutil.py
--- a/mercurial/logcmdutil.py
+++ b/mercurial/logcmdutil.py
@@ -35,7 +35,10 @@  from . import (
     templater,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 def getlimit(opts):
     """get the log limit according to option -l/--limit"""
@@ -260,7 +263,8 @@  class changesetprinter(object):
         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()
diff --git a/mercurial/mail.py b/mercurial/mail.py
--- a/mercurial/mail.py
+++ b/mercurial/mail.py
@@ -24,6 +24,9 @@  from . import (
     sslutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class STARTTLS(smtplib.SMTP):
     '''Derived class to verify the peer certificate for STARTTLS.
@@ -81,7 +84,7 @@  def _smtp(ui):
     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"))
@@ -137,8 +140,8 @@  def _smtp(ui):
 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)
diff --git a/mercurial/match.py b/mercurial/match.py
--- a/mercurial/match.py
+++ b/mercurial/match.py
@@ -19,6 +19,9 @@  from . import (
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 allpatternkinds = ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
                    'listfile', 'listfile0', 'set', 'include', 'subinclude',
@@ -227,7 +230,7 @@  def _donormalize(patterns, default, root
             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, ''))
diff --git a/mercurial/minirst.py b/mercurial/minirst.py
--- a/mercurial/minirst.py
+++ b/mercurial/minirst.py
@@ -27,7 +27,9 @@  from . import (
     encoding,
     pycompat,
     url,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 def section(s):
@@ -459,9 +461,9 @@  def formatoption(block, width):
     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."""
@@ -477,9 +479,9 @@  def formatblock(block, width):
         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':
@@ -503,7 +505,9 @@  def formatblock(block, width):
                 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:
@@ -514,9 +518,9 @@  def formatblock(block, width):
         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('| '):
@@ -540,9 +544,9 @@  def formatblock(block, width):
         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"""
diff --git a/mercurial/parser.py b/mercurial/parser.py
--- a/mercurial/parser.py
+++ b/mercurial/parser.py
@@ -25,6 +25,9 @@  from . import (
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class parser(object):
     def __init__(self, elements, methods=None):
@@ -190,14 +193,14 @@  def buildargsdict(trees, funcname, argsp
 
 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):
diff --git a/mercurial/patch.py b/mercurial/patch.py
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -40,7 +40,10 @@  from . import (
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 diffhelpers = policy.importmod(r'diffhelpers')
 stringio = util.stringio
@@ -1461,7 +1464,7 @@  class binhunk(object):
                 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:
diff --git a/mercurial/repair.py b/mercurial/repair.py
--- a/mercurial/repair.py
+++ b/mercurial/repair.py
@@ -26,6 +26,9 @@  from . import (
     obsutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 def backupbundle(repo, bases, heads, node, suffix, compress=True,
                  obsolescence=True):
@@ -236,7 +239,8 @@  def strip(ui, repo, nodelist, backup=Tru
             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:
diff --git a/mercurial/revlog.py b/mercurial/revlog.py
--- a/mercurial/revlog.py
+++ b/mercurial/revlog.py
@@ -45,6 +45,9 @@  from . import (
     templatefilters,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 parsers = policy.importmod(r'parsers')
 
@@ -2017,7 +2020,7 @@  class revlog(object):
                 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
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -31,7 +31,10 @@  from . import (
     stack,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 # helpers for processing parsed tree
 getsymbol = revsetlang.getsymbol
@@ -447,7 +450,7 @@  def bookmark(repo, subset, x):
         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)
@@ -492,7 +495,7 @@  def branch(repo, subset, x):
         # 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
@@ -819,7 +822,7 @@  def extra(repo, subset, x):
         # 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()
@@ -1014,7 +1017,7 @@  def grep(repo, subset, x):
         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]
@@ -1286,7 +1289,7 @@  def named(repo, subset, x):
     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:
@@ -1942,7 +1945,7 @@  def subrepo(repo, subset, x):
     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
@@ -1995,8 +1998,8 @@  def successors(repo, subset, x):
     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)
@@ -2019,7 +2022,7 @@  def tag(repo, subset, x):
         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)
diff --git a/mercurial/revsetlang.py b/mercurial/revsetlang.py
--- a/mercurial/revsetlang.py
+++ b/mercurial/revsetlang.py
@@ -17,6 +17,9 @@  from . import (
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
@@ -207,7 +210,7 @@  def getinteger(x, err, default=_notset):
         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)
@@ -565,7 +568,7 @@  def _quote(s):
     >>> _quote(1)
     "'1'"
     """
-    return "'%s'" % util.escapestr(pycompat.bytestr(s))
+    return "'%s'" % stringutil.escapestr(pycompat.bytestr(s))
 
 def _formatargtype(c, arg):
     if c == 'd':
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -41,6 +41,10 @@  from . import (
     vfs,
 )
 
+from .utils import (
+    stringutil,
+)
+
 if pycompat.iswindows:
     from . import scmwindows as scmplatform
 else:
@@ -163,12 +167,12 @@  def callcatch(ui, func):
         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:
@@ -194,7 +198,7 @@  def callcatch(ui, func):
         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:
@@ -211,15 +215,15 @@  def callcatch(ui, func):
         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]
@@ -237,7 +241,7 @@  def callcatch(ui, func):
             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:
@@ -246,7 +250,7 @@  def callcatch(ui, func):
         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:
@@ -256,7 +260,7 @@  def callcatch(ui, func):
         # 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
 
@@ -299,7 +303,7 @@  def checkportabilityalert(ui):
     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'):
diff --git a/mercurial/simplemerge.py b/mercurial/simplemerge.py
--- a/mercurial/simplemerge.py
+++ b/mercurial/simplemerge.py
@@ -23,7 +23,9 @@  from . import (
     error,
     mdiff,
     pycompat,
-    util,
+)
+from .utils import (
+    stringutil,
 )
 
 class CantReprocessAndShowBase(Exception):
@@ -397,7 +399,7 @@  class Merge3Text(object):
 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)
diff --git a/mercurial/sslutil.py b/mercurial/sslutil.py
--- a/mercurial/sslutil.py
+++ b/mercurial/sslutil.py
@@ -21,6 +21,9 @@  from . import (
     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
@@ -374,7 +377,8 @@  def wrapsocket(sock, keyfile, certfile, 
             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'])
 
@@ -393,7 +397,7 @@  def wrapsocket(sock, keyfile, certfile, 
             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']:
@@ -642,7 +646,7 @@  def _verifycert(cert, hostname):
                 if _dnsnamematch(value, hostname):
                     return
             except wildcarderror as e:
-                return util.forcebytestr(e.args[0])
+                return stringutil.forcebytestr(e.args[0])
 
             dnsnames.append(value)
 
@@ -663,7 +667,7 @@  def _verifycert(cert, hostname):
                         if _dnsnamematch(value, hostname):
                             return
                     except wildcarderror as e:
-                        return util.forcebytestr(e.args[0])
+                        return stringutil.forcebytestr(e.args[0])
 
                     dnsnames.append(value)
 
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -36,7 +36,10 @@  from . import (
     util,
     vfs as vfsmod,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 hg = None
 reporelpath = subrepoutil.reporelpath
@@ -74,7 +77,7 @@  def annotatesubrepoerror(func):
             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,
diff --git a/mercurial/subrepoutil.py b/mercurial/subrepoutil.py
--- a/mercurial/subrepoutil.py
+++ b/mercurial/subrepoutil.py
@@ -21,6 +21,9 @@  from . import (
     phases,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 nullstate = ('', '', 'empty')
 
@@ -74,7 +77,7 @@  def state(ctx, ui):
         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.
diff --git a/mercurial/tags.py b/mercurial/tags.py
--- a/mercurial/tags.py
+++ b/mercurial/tags.py
@@ -28,6 +28,9 @@  from . import (
     scmutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 # Tags computation can be expensive and caches exist to make it fast in
 # the common case.
@@ -783,6 +786,6 @@  class hgtagsfnodescache(object):
         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()
diff --git a/mercurial/templatefilters.py b/mercurial/templatefilters.py
--- a/mercurial/templatefilters.py
+++ b/mercurial/templatefilters.py
@@ -21,7 +21,10 @@  from . import (
     url,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -128,7 +131,7 @@  def email(text):
     address. Example: ``User <user@example.com>`` becomes
     ``user@example.com``.
     """
-    return util.email(text)
+    return stringutil.email(text)
 
 @templatefilter('escape')
 def escape(text):
@@ -162,8 +165,9 @@  def fill(text, width, initindent='', han
             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')
@@ -369,7 +373,7 @@  def splitlines(text):
 
 @templatefilter('stringescape')
 def stringescape(text):
-    return util.escapestr(text)
+    return stringutil.escapestr(text)
 
 @templatefilter('stringify')
 def stringify(thing):
@@ -412,12 +416,12 @@  def urlescape(text):
 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):
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -26,6 +26,9 @@  from . import (
     templateutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 _hybrid = templateutil.hybrid
 _mappable = templateutil.mappable
@@ -72,7 +75,7 @@  def getlatesttags(context, mapping, patt
     cachename = 'latesttags'
     if pattern is not None:
         cachename += '-' + pattern
-        match = util.stringmatcher(pattern)[2]
+        match = stringutil.stringmatcher(pattern)[2]
     else:
         match = util.always
 
@@ -307,7 +310,7 @@  def showextras(context, mapping):
     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')
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -63,6 +63,9 @@  from . import (
     templateutil,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 # template parsing
 
@@ -811,7 +814,8 @@  class templater(object):
                     _('"%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]
 
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -15,6 +15,9 @@  from . import (
     pycompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 class ResourceUnavailable(error.Abort):
     pass
@@ -281,7 +284,7 @@  def evalboolean(context, mapping, arg):
         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)
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -37,7 +37,10 @@  from . import (
     scmutil,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 urlreq = util.urlreq
 
@@ -371,7 +374,7 @@  class ui(object):
         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',
@@ -591,7 +594,7 @@  class ui(object):
             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))
@@ -821,7 +824,7 @@  class ui(object):
     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):
diff --git a/mercurial/url.py b/mercurial/url.py
--- a/mercurial/url.py
+++ b/mercurial/url.py
@@ -24,6 +24,9 @@  from . import (
     urllibcompat,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 httplib = util.httplib
 stringio = util.stringio
@@ -477,7 +480,7 @@  class cookiehandler(urlreq.basehandler):
             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:
diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -820,9 +820,10 @@  class baseproxyobserver(object):
         # 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
 
@@ -832,7 +833,8 @@  class baseproxyobserver(object):
 
         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):
@@ -1915,7 +1917,7 @@  def checkwinfilename(path):
                          "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 "
@@ -3679,7 +3681,7 @@  class _zlibengine(compressionengine):
                 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()
@@ -3905,7 +3907,7 @@  class _zstdengine(compressionengine):
                 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 {}
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -34,6 +34,10 @@  from . import (
     wireprototypes,
 )
 
+from .utils import (
+    stringutil,
+)
+
 urlerr = util.urlerr
 urlreq = util.urlreq
 
@@ -994,7 +998,7 @@  def lookup(repo, proto, key):
         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))
 
@@ -1007,7 +1011,7 @@  def known(repo, proto, nodes, others):
 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')
@@ -1123,7 +1127,7 @@  def unbundle(repo, proto, heads):
                 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))
@@ -1131,5 +1135,5 @@  def unbundle(repo, proto, heads):
                                                    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())
diff --git a/mercurial/wireprotoframing.py b/mercurial/wireprotoframing.py
--- a/mercurial/wireprotoframing.py
+++ b/mercurial/wireprotoframing.py
@@ -21,6 +21,9 @@  from . import (
     error,
     util,
 )
+from .utils import (
+    stringutil,
+)
 
 FRAME_HEADER_SIZE = 6
 DEFAULT_MAX_FRAME_SIZE = 32768
@@ -164,7 +167,7 @@  def makeframefromhumanstring(s):
         else:
             finalflags |= int(flag)
 
-    payload = util.unescapestr(payload)
+    payload = stringutil.unescapestr(payload)
 
     return makeframe(requestid=requestid, typeid=frametype,
                      flags=finalflags, payload=payload)
diff --git a/tests/test-simplemerge.py b/tests/test-simplemerge.py
--- a/tests/test-simplemerge.py
+++ b/tests/test-simplemerge.py
@@ -22,6 +22,10 @@  from mercurial import (
     util,
 )
 
+from mercurial.utils import (
+    stringutil,
+)
+
 TestCase = unittest.TestCase
 # bzr compatible interface, for the tests
 class Merge3(simplemerge.Merge3Text):
@@ -34,7 +38,8 @@  class Merge3(simplemerge.Merge3Text):
         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)
@@ -358,4 +363,3 @@  if __name__ == '__main__':
         unittest.main()
     finally:
         time.time = orig
-