changeset 36636:c6061cadb400

util: extract all date-related utils in utils/dateutil module With this commit, util.py lose 262 lines Note for extensions author, if this commit breaks your extension, you can pull the step-by-step split here to help you more easily pinpoint the renaming that broke your extension: hg pull https://bitbucket.org/octobus/mercurial-devel/ -r ac1f6453010d Differential Revision: https://phab.mercurial-scm.org/D2282
author Boris Feld <boris.feld@octobus.net>
date Thu, 15 Feb 2018 17:18:26 +0100
parents 4de15c54e59f
children 1151c731686e
files contrib/synthrepo.py hgext/blackbox.py hgext/churn.py hgext/convert/common.py hgext/convert/convcmd.py hgext/convert/cvs.py hgext/convert/cvsps.py hgext/convert/darcs.py hgext/convert/gnuarch.py hgext/convert/hg.py hgext/convert/monotone.py hgext/convert/p4.py hgext/convert/subversion.py hgext/fetch.py hgext/gpg.py hgext/journal.py hgext/keyword.py hgext/mq.py hgext/notify.py hgext/patchbomb.py hgext/shelve.py mercurial/changelog.py mercurial/cmdutil.py mercurial/commands.py mercurial/context.py mercurial/debugcommands.py mercurial/formatter.py mercurial/hgweb/hgwebdir_mod.py mercurial/logcmdutil.py mercurial/mdiff.py mercurial/obsolete.py mercurial/obsutil.py mercurial/patch.py mercurial/revset.py mercurial/subrepo.py mercurial/templatefilters.py mercurial/templater.py mercurial/ui.py mercurial/util.py mercurial/utils/__init__.py mercurial/utils/dateutil.py setup.py tests/fakedirstatewritetime.py tests/fakepatchtime.py tests/test-journal.t
diffstat 44 files changed, 516 insertions(+), 405 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/synthrepo.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/contrib/synthrepo.py	Thu Feb 15 17:18:26 2018 +0100
@@ -59,8 +59,8 @@
     patch,
     registrar,
     scmutil,
-    util,
 )
+from mercurial.utils import dateutil
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -382,7 +382,7 @@
         message = 'synthesized wide repo with %d files' % (len(files),)
         mc = context.memctx(repo, [pctx.node(), nullid], message,
                             files, filectxfn, ui.username(),
-                            '%d %d' % util.makedate())
+                            '%d %d' % dateutil.makedate())
         initnode = mc.commit()
         if ui.debugflag:
             hexfn = hex
--- a/hgext/blackbox.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/blackbox.py	Thu Feb 15 17:18:26 2018 +0100
@@ -49,6 +49,7 @@
     ui as uimod,
     util,
 )
+from mercurial.utils import dateutil
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -164,7 +165,7 @@
                 return
             ui._bbinlog = True
             default = self.configdate('devel', 'default-date')
-            date = util.datestr(default, '%Y/%m/%d %H:%M:%S')
+            date = dateutil.datestr(default, '%Y/%m/%d %H:%M:%S')
             user = util.getuser()
             pid = '%d' % util.getpid()
             formattedmsg = msg[0] % msg[1:]
--- a/hgext/churn.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/churn.py	Thu Feb 15 17:18:26 2018 +0100
@@ -23,8 +23,8 @@
     pycompat,
     registrar,
     scmutil,
-    util,
 )
+from mercurial.utils import dateutil
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -65,7 +65,7 @@
     rate = {}
     df = False
     if opts.get('date'):
-        df = util.matchdate(opts['date'])
+        df = dateutil.matchdate(opts['date'])
 
     m = scmutil.match(repo[None], pats, opts)
     def prep(ctx, fns):
--- a/hgext/convert/common.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/common.py	Thu Feb 15 17:18:26 2018 +0100
@@ -541,7 +541,7 @@
             self.fp = None
 
 def makedatetimestamp(t):
-    """Like util.makedate() but for time t instead of current time"""
+    """Like dateutil.makedate() but for time t instead of current time"""
     delta = (datetime.datetime.utcfromtimestamp(t) -
              datetime.datetime.fromtimestamp(t))
     tz = delta.days * 86400 + delta.seconds
--- a/hgext/convert/convcmd.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/convcmd.py	Thu Feb 15 17:18:26 2018 +0100
@@ -19,6 +19,7 @@
     scmutil,
     util,
 )
+from mercurial.utils import dateutil
 
 from . import (
     bzr,
@@ -355,7 +356,7 @@
             dates = {}
             def getdate(n):
                 if n not in dates:
-                    dates[n] = util.parsedate(self.commitcache[n].date)
+                    dates[n] = dateutil.parsedate(self.commitcache[n].date)
                 return dates[n]
 
             def picknext(nodes):
--- a/hgext/convert/cvs.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/cvs.py	Thu Feb 15 17:18:26 2018 +0100
@@ -18,6 +18,7 @@
     pycompat,
     util,
 )
+from mercurial.utils import dateutil
 
 from . import (
     common,
@@ -93,7 +94,7 @@
                 cs.comment = self.recode(cs.comment)
                 if self.ui.configbool('convert', 'localtimezone'):
                     cs.date = makedatetimestamp(cs.date[0])
-                date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
+                date = dateutil.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
                 self.tags.update(dict.fromkeys(cs.tags, id))
 
                 files = {}
--- a/hgext/convert/cvsps.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/cvsps.py	Thu Feb 15 17:18:26 2018 +0100
@@ -17,6 +17,7 @@
     pycompat,
     util,
 )
+from mercurial.utils import dateutil
 
 pickle = util.pickle
 
@@ -192,7 +193,7 @@
 
         if oldlog:
             date = oldlog[-1].date    # last commit date as a (time,tz) tuple
-            date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
+            date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
 
     # build the CVS commandline
     cmd = ['cvs', '-q']
@@ -336,7 +337,7 @@
             if len(d.split()) != 3:
                 # cvs log dates always in GMT
                 d = d + ' UTC'
-            e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
+            e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
                                         '%Y/%m/%d %H:%M:%S',
                                         '%Y-%m-%d %H:%M:%S'])
             e.author = scache(match.group(2))
@@ -901,7 +902,7 @@
             #       bug-for-bug compatibility with cvsps.
             ui.write('---------------------\n')
             ui.write(('PatchSet %d \n' % cs.id))
-            ui.write(('Date: %s\n' % util.datestr(cs.date,
+            ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
                                                  '%Y/%m/%d %H:%M:%S %1%2')))
             ui.write(('Author: %s\n' % cs.author))
             ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
--- a/hgext/convert/darcs.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/darcs.py	Thu Feb 15 17:18:26 2018 +0100
@@ -16,6 +16,7 @@
     error,
     util,
 )
+from mercurial.utils import dateutil
 from . import common
 NoRepo = common.NoRepo
 
@@ -148,12 +149,14 @@
 
     def getcommit(self, rev):
         elt = self.changes[rev]
-        date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
+        dateformat = '%a %b %d %H:%M:%S %Z %Y'
+        date = dateutil.strdate(elt.get('local_date'), dateformat)
         desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
         # etree can return unicode objects for name, comment, and author,
         # so recode() is used to ensure str objects are emitted.
+        newdateformat = '%Y-%m-%d %H:%M:%S %1%2'
         return common.commit(author=self.recode(elt.get('author')),
-                             date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
+                             date=dateutil.datestr(date, newdateformat),
                              desc=self.recode(desc).strip(),
                              parents=self.parents[rev])
 
--- a/hgext/convert/gnuarch.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/gnuarch.py	Thu Feb 15 17:18:26 2018 +0100
@@ -19,6 +19,7 @@
     error,
     util,
 )
+from mercurial.utils import dateutil
 from . import common
 
 class gnuarch_source(common.converter_source, common.commandline):
@@ -280,8 +281,8 @@
             catlog = self.catlogparser.parsestr(data)
 
             # Commit date
-            self.changes[rev].date = util.datestr(
-                util.strdate(catlog['Standard-date'],
+            self.changes[rev].date = dateutil.datestr(
+                dateutil.strdate(catlog['Standard-date'],
                              '%Y-%m-%d %H:%M:%S'))
 
             # Commit author
--- a/hgext/convert/hg.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/hg.py	Thu Feb 15 17:18:26 2018 +0100
@@ -36,6 +36,7 @@
     scmutil,
     util,
 )
+from mercurial.utils import dateutil
 stringio = util.stringio
 
 from . import common
@@ -583,7 +584,7 @@
         crev = rev
 
         return common.commit(author=ctx.user(),
-                             date=util.datestr(ctx.date(),
+                             date=dateutil.datestr(ctx.date(),
                                                '%Y-%m-%d %H:%M:%S %1%2'),
                              desc=ctx.description(),
                              rev=crev,
--- a/hgext/convert/monotone.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/monotone.py	Thu Feb 15 17:18:26 2018 +0100
@@ -14,8 +14,8 @@
 from mercurial import (
     error,
     pycompat,
-    util,
 )
+from mercurial.utils import dateutil
 
 from . import common
 
@@ -310,9 +310,10 @@
         certs = self.mtngetcerts(rev)
         if certs.get('suspend') == certs["branch"]:
             extra['close'] = 1
+        dateformat = "%Y-%m-%dT%H:%M:%S"
         return common.commit(
             author=certs["author"],
-            date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
+            date=dateutil.datestr(dateutil.strdate(certs["date"], dateformat)),
             desc=certs["changelog"],
             rev=rev,
             parents=self.mtnrun("parents", rev).splitlines(),
--- a/hgext/convert/p4.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/p4.py	Thu Feb 15 17:18:26 2018 +0100
@@ -14,6 +14,7 @@
     error,
     util,
 )
+from mercurial.utils import dateutil
 
 from . import common
 
@@ -346,7 +347,7 @@
             parents = []
 
         return common.commit(author=self.recode(obj["user"]),
-            date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
+            date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
             parents=parents, desc=desc, branch=None, rev=obj['change'],
             extra={"p4": obj['change'], "convert_revision": obj['change']})
 
--- a/hgext/convert/subversion.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/convert/subversion.py	Thu Feb 15 17:18:26 2018 +0100
@@ -16,6 +16,7 @@
     util,
     vfs as vfsmod,
 )
+from mercurial.utils import dateutil
 
 from . import common
 
@@ -891,7 +892,7 @@
             # Example SVN datetime. Includes microseconds.
             # ISO-8601 conformant
             # '2007-01-04T17:35:00.902377Z'
-            date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
+            date = dateutil.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
             if self.ui.configbool('convert', 'localtimezone'):
                 date = makedatetimestamp(date[0])
 
@@ -913,7 +914,7 @@
                 branch = None
 
             cset = commit(author=author,
-                          date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
+                          date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
                           desc=log,
                           parents=parents,
                           branch=branch,
--- a/hgext/fetch.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/fetch.py	Thu Feb 15 17:18:26 2018 +0100
@@ -23,6 +23,7 @@
     registrar,
     util,
 )
+from mercurial.utils import dateutil
 
 release = lock.release
 cmdtable = {}
@@ -64,7 +65,7 @@
     opts = pycompat.byteskwargs(opts)
     date = opts.get('date')
     if date:
-        opts['date'] = util.parsedate(date)
+        opts['date'] = dateutil.parsedate(date)
 
     parent, _p2 = repo.dirstate.parents()
     branch = repo.dirstate.branch()
--- a/hgext/gpg.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/gpg.py	Thu Feb 15 17:18:26 2018 +0100
@@ -21,6 +21,7 @@
     registrar,
     util,
 )
+from mercurial.utils import dateutil
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -258,7 +259,7 @@
 
     date = opts.get('date')
     if date:
-        opts['date'] = util.parsedate(date)
+        opts['date'] = dateutil.parsedate(date)
 
     if revs:
         nodes = [repo.lookup(n) for n in revs]
--- a/hgext/journal.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/journal.py	Thu Feb 15 17:18:26 2018 +0100
@@ -35,6 +35,7 @@
     registrar,
     util,
 )
+from mercurial.utils import dateutil
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -326,7 +327,7 @@
             newhashes = [newhashes]
 
         entry = journalentry(
-            util.makedate(), self.user, self.command, namespace, name,
+            dateutil.makedate(), self.user, self.command, namespace, name,
             oldhashes, newhashes)
 
         vfs = self.vfs
--- a/hgext/keyword.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/keyword.py	Thu Feb 15 17:18:26 2018 +0100
@@ -111,6 +111,7 @@
     templatefilters,
     util,
 )
+from mercurial.utils import dateutil
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -156,21 +157,23 @@
 def utcdate(text):
     '''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
     '''
-    return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
+    dateformat = '%Y/%m/%d %H:%M:%S'
+    return dateutil.datestr((dateutil.parsedate(text)[0], 0), dateformat)
 # date like in svn's $Date
 @templatefilter('svnisodate')
 def svnisodate(text):
     '''Date. Returns a date in this format: "2009-08-18 13:00:13
     +0200 (Tue, 18 Aug 2009)".
     '''
-    return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
+    return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
 # date like in svn's $Id
 @templatefilter('svnutcdate')
 def svnutcdate(text):
     '''Date. Returns a UTC-date in this format: "2009-08-18
     11:00:13Z".
     '''
-    return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
+    dateformat = '%Y-%m-%d %H:%M:%SZ'
+    return dateutil.datestr((dateutil.parsedate(text)[0], 0), dateformat)
 
 # make keyword tools accessible
 kwtools = {'hgcmd': ''}
--- a/hgext/mq.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/mq.py	Thu Feb 15 17:18:26 2018 +0100
@@ -98,6 +98,7 @@
     util,
     vfs as vfsmod,
 )
+from mercurial.utils import dateutil
 
 release = lockmod.release
 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
@@ -1201,7 +1202,7 @@
         user = opts.get('user')
         date = opts.get('date')
         if date:
-            date = util.parsedate(date)
+            date = dateutil.parsedate(date)
         diffopts = self.diffopts({'git': opts.get('git')}, plain=True)
         if opts.get('checkname', True):
             self.checkpatchname(patchfn)
@@ -1644,7 +1645,7 @@
         newuser = opts.get('user')
         newdate = opts.get('date')
         if newdate:
-            newdate = '%d %d' % util.parsedate(newdate)
+            newdate = '%d %d' % dateutil.parsedate(newdate)
         wlock = repo.wlock()
 
         try:
@@ -2596,7 +2597,7 @@
     if not opts.get('user') and opts.get('currentuser'):
         opts['user'] = ui.username()
     if not opts.get('date') and opts.get('currentdate'):
-        opts['date'] = "%d %d" % util.makedate()
+        opts['date'] = "%d %d" % dateutil.makedate()
 
 @command("^qnew",
          [('e', 'edit', None, _('invoke editor on commit messages')),
--- a/hgext/notify.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/notify.py	Thu Feb 15 17:18:26 2018 +0100
@@ -149,6 +149,7 @@
     registrar,
     util,
 )
+from mercurial.utils import dateutil
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -360,7 +361,7 @@
             for k, v in headers:
                 msg[k] = v
 
-        msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
+        msg['Date'] = dateutil.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
 
         # try to make subject line exist and be useful
         if not subject:
--- a/hgext/patchbomb.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/patchbomb.py	Thu Feb 15 17:18:26 2018 +0100
@@ -99,6 +99,7 @@
     templater,
     util,
 )
+from mercurial.utils import dateutil
 stringio = util.stringio
 
 cmdtable = {}
@@ -665,9 +666,9 @@
 
     # start
     if date:
-        start_time = util.parsedate(date)
+        start_time = dateutil.parsedate(date)
     else:
-        start_time = util.makedate()
+        start_time = dateutil.makedate()
 
     def genmsgid(id):
         return '<%s.%d@%s>' % (id[:20], int(start_time[0]),
--- a/hgext/shelve.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/hgext/shelve.py	Thu Feb 15 17:18:26 2018 +0100
@@ -55,6 +55,7 @@
 from . import (
     rebase,
 )
+from mercurial.utils import dateutil
 
 cmdtable = {}
 command = registrar.command(cmdtable)
@@ -563,7 +564,8 @@
             continue
         ui.write(' ' * (16 - len(sname)))
         used = 16
-        age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
+        date = dateutil.makedate(mtime)
+        age = '(%s)' % templatefilters.age(date, abbrev=True)
         ui.write(age, label='shelve.age')
         ui.write(' ' * (12 - len(age)))
         used += 12
--- a/mercurial/changelog.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/changelog.py	Thu Feb 15 17:18:26 2018 +0100
@@ -24,6 +24,7 @@
     revlog,
     util,
 )
+from .utils import dateutil
 
 _defaultextra = {'branch': 'default'}
 
@@ -524,9 +525,9 @@
         desc = stripdesc(desc)
 
         if date:
-            parseddate = "%d %d" % util.parsedate(date)
+            parseddate = "%d %d" % dateutil.parsedate(date)
         else:
-            parseddate = "%d %d" % util.makedate()
+            parseddate = "%d %d" % dateutil.makedate()
         if extra:
             branch = extra.get("branch")
             if branch in ("default", ""):
--- a/mercurial/cmdutil.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/cmdutil.py	Thu Feb 15 17:18:26 2018 +0100
@@ -47,6 +47,7 @@
     util,
     vfs as vfsmod,
 )
+from .utils import dateutil
 stringio = util.stringio
 
 # templates of common command options
@@ -1530,7 +1531,7 @@
     write("# HG changeset patch\n")
     write("# User %s\n" % ctx.user())
     write("# Date %d %d\n" % ctx.date())
-    write("#      %s\n" % util.datestr(ctx.date()))
+    write("#      %s\n" % dateutil.datestr(ctx.date()))
     if branch and branch != 'default':
         write("# Branch %s\n" % branch)
     write("# Node ID %s\n" % hex(node))
@@ -1629,7 +1630,7 @@
 def finddate(ui, repo, date):
     """Find the tipmost changeset that matches the given date spec"""
 
-    df = util.matchdate(date)
+    df = dateutil.matchdate(date)
     m = scmutil.matchall(repo)
     results = {}
 
@@ -1642,7 +1643,7 @@
         rev = ctx.rev()
         if rev in results:
             ui.status(_("found revision %s from %s\n") %
-                      (rev, util.datestr(results[rev])))
+                      (rev, dateutil.datestr(results[rev])))
             return '%d' % rev
 
     raise error.Abort(_("revision matching date not found"))
@@ -2261,7 +2262,7 @@
     '''commit the specified files or all outstanding changes'''
     date = opts.get('date')
     if date:
-        opts['date'] = util.parsedate(date)
+        opts['date'] = dateutil.parsedate(date)
     message = logmessage(ui, opts)
     matcher = scmutil.match(repo[None], pats, opts)
 
@@ -2326,7 +2327,7 @@
         date = opts.get('date') or old.date()
 
         # Parse the date to allow comparison between date and old.date()
-        date = util.parsedate(date)
+        date = dateutil.parsedate(date)
 
         if len(old.parents()) > 1:
             # ctx.files() isn't reliable for merges, so fall back to the
--- a/mercurial/commands.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/commands.py	Thu Feb 15 17:18:26 2018 +0100
@@ -61,6 +61,7 @@
     util,
     wireprotoserver,
 )
+from .utils import dateutil
 
 release = lockmod.release
 
@@ -302,9 +303,9 @@
 
     rootfm = ui.formatter('annotate', opts)
     if ui.quiet:
-        datefunc = util.shortdate
+        datefunc = dateutil.shortdate
     else:
-        datefunc = util.datestr
+        datefunc = dateutil.datestr
     if ctx.rev() is None:
         def hexfn(node):
             if node is None:
@@ -584,7 +585,7 @@
 
     date = opts.get('date')
     if date:
-        opts['date'] = util.parsedate(date)
+        opts['date'] = dateutil.parsedate(date)
 
     cmdutil.checkunfinished(repo)
     cmdutil.bailifchanged(repo)
@@ -2161,7 +2162,7 @@
     if not opts.get('user') and opts.get('currentuser'):
         opts['user'] = ui.username()
     if not opts.get('date') and opts.get('currentdate'):
-        opts['date'] = "%d %d" % util.makedate()
+        opts['date'] = "%d %d" % dateutil.makedate()
 
     editor = cmdutil.getcommiteditor(editform='graft',
                                      **pycompat.strkwargs(opts))
@@ -3011,7 +3012,7 @@
 
     date = opts.get('date')
     if date:
-        opts['date'] = util.parsedate(date)
+        opts['date'] = dateutil.parsedate(date)
 
     exact = opts.get('exact')
     update = not opts.get('bypass')
@@ -5307,7 +5308,7 @@
 
         date = opts.get('date')
         if date:
-            date = util.parsedate(date)
+            date = dateutil.parsedate(date)
 
         if opts.get('remove'):
             editform = 'tag.remove'
--- a/mercurial/context.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/context.py	Thu Feb 15 17:18:26 2018 +0100
@@ -49,6 +49,7 @@
     subrepoutil,
     util,
 )
+from .utils import dateutil
 
 propertycache = util.propertycache
 
@@ -1287,7 +1288,7 @@
         self._node = None
         self._text = text
         if date:
-            self._date = util.parsedate(date)
+            self._date = dateutil.parsedate(date)
         if user:
             self._user = user
         if changes:
@@ -1364,7 +1365,7 @@
         ui = self._repo.ui
         date = ui.configdate('devel', 'default-date')
         if date is None:
-            date = util.makedate()
+            date = dateutil.makedate()
         return date
 
     def subrev(self, subpath):
@@ -2111,11 +2112,11 @@
         if data is None:
             raise error.ProgrammingError("data must be non-None")
         self._auditconflicts(path)
-        self._markdirty(path, exists=True, data=data, date=util.makedate(),
+        self._markdirty(path, exists=True, data=data, date=dateutil.makedate(),
                         flags=flags)
 
     def setflags(self, path, l, x):
-        self._markdirty(path, exists=True, date=util.makedate(),
+        self._markdirty(path, exists=True, date=dateutil.makedate(),
                         flags=(l and 'l' or '') + (x and 'x' or ''))
 
     def remove(self, path):
@@ -2404,7 +2405,7 @@
 
     user receives the committer name and defaults to current
     repository username, date is the commit date in any format
-    supported by util.parsedate() and defaults to current date, extra
+    supported by dateutil.parsedate() and defaults to current date, extra
     is a dictionary of metadata or is left empty.
     """
 
@@ -2619,7 +2620,7 @@
 
     user receives the committer name and defaults to current repository
     username, date is the commit date in any format supported by
-    util.parsedate() and defaults to current date, extra is a dictionary of
+    dateutil.parsedate() and defaults to current date, extra is a dictionary of
     metadata or is left empty.
     """
     def __new__(cls, repo, originalctx, *args, **kwargs):
--- a/mercurial/debugcommands.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/debugcommands.py	Thu Feb 15 17:18:26 2018 +0100
@@ -77,6 +77,7 @@
     vfs as vfsmod,
     wireprotoserver,
 )
+from .utils import dateutil
 
 release = lockmod.release
 
@@ -560,13 +561,13 @@
 def debugdate(ui, date, range=None, **opts):
     """parse and display a date"""
     if opts[r"extended"]:
-        d = util.parsedate(date, util.extendeddateformats)
+        d = dateutil.parsedate(date, util.extendeddateformats)
     else:
-        d = util.parsedate(date)
+        d = dateutil.parsedate(date)
     ui.write(("internal: %d %d\n") % d)
-    ui.write(("standard: %s\n") % util.datestr(d))
+    ui.write(("standard: %s\n") % dateutil.datestr(d))
     if range:
-        m = util.matchdate(range)
+        m = dateutil.matchdate(range)
         ui.write(("match: %s\n") % m(d[0]))
 
 @command('debugdeltachain',
@@ -1578,7 +1579,7 @@
             try:
                 date = opts.get('date')
                 if date:
-                    date = util.parsedate(date)
+                    date = dateutil.parsedate(date)
                 else:
                     date = None
                 prec = parsenodeid(precursor)
--- a/mercurial/formatter.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/formatter.py	Thu Feb 15 17:18:26 2018 +0100
@@ -126,6 +126,7 @@
     templater,
     util,
 )
+from .utils import dateutil
 
 pickle = util.pickle
 
@@ -243,7 +244,7 @@
     @staticmethod
     def formatdate(date, fmt):
         '''stringify date tuple in the given format'''
-        return util.datestr(date, fmt)
+        return dateutil.datestr(date, fmt)
     @staticmethod
     def formatdict(data, key, value, fmt, sep):
         '''stringify key-value pairs separated by sep'''
--- a/mercurial/hgweb/hgwebdir_mod.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/hgweb/hgwebdir_mod.py	Thu Feb 15 17:18:26 2018 +0100
@@ -46,6 +46,7 @@
     webutil,
     wsgicgi,
 )
+from ..utils import dateutil
 
 def cleannames(items):
     return [(util.pconvert(name).strip('/'), path) for name, path in items]
@@ -376,7 +377,7 @@
                 if directory:
                     # get the directory's time information
                     try:
-                        d = (get_mtime(path), util.makedate()[1])
+                        d = (get_mtime(path), dateutil.makedate()[1])
                     except OSError:
                         continue
 
@@ -425,7 +426,7 @@
                     u.warn(_('error accessing repository at %s\n') % path)
                     continue
                 try:
-                    d = (get_mtime(r.spath), util.makedate()[1])
+                    d = (get_mtime(r.spath), dateutil.makedate()[1])
                 except OSError:
                     continue
 
--- a/mercurial/logcmdutil.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/logcmdutil.py	Thu Feb 15 17:18:26 2018 +0100
@@ -35,6 +35,7 @@
     templater,
     util,
 )
+from .utils import dateutil
 
 def getlimit(opts):
     """get the log limit according to option -l/--limit"""
@@ -229,7 +230,7 @@
                           % scmutil.formatrevnode(self.ui, mrev, mnode),
                           label='ui.debug log.manifest')
         self.ui.write(columns['user'] % ctx.user(), label='log.user')
-        self.ui.write(columns['date'] % util.datestr(ctx.date()),
+        self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
                       label='log.date')
 
         if ctx.isunstable():
--- a/mercurial/mdiff.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/mdiff.py	Thu Feb 15 17:18:26 2018 +0100
@@ -19,6 +19,7 @@
     pycompat,
     util,
 )
+from .utils import dateutil
 
 _missing_newline_marker = "\\ No newline at end of file\n"
 
@@ -255,7 +256,7 @@
         aprefix = 'a/'
         bprefix = 'b/'
 
-    epoch = util.datestr((0, 0))
+    epoch = dateutil.datestr((0, 0))
 
     fn1 = util.pconvert(fn1)
     fn2 = util.pconvert(fn2)
--- a/mercurial/obsolete.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/obsolete.py	Thu Feb 15 17:18:26 2018 +0100
@@ -81,6 +81,7 @@
     policy,
     util,
 )
+from .utils import dateutil
 
 parsers = policy.importmod(r'parsers')
 
@@ -601,13 +602,13 @@
         if date is None:
             if 'date' in metadata:
                 # as a courtesy for out-of-tree extensions
-                date = util.parsedate(metadata.pop('date'))
+                date = dateutil.parsedate(metadata.pop('date'))
             elif ui is not None:
                 date = ui.configdate('devel', 'default-date')
                 if date is None:
-                    date = util.makedate()
+                    date = dateutil.makedate()
             else:
-                date = util.makedate()
+                date = dateutil.makedate()
         if len(prec) != 20:
             raise ValueError(prec)
         for succ in succs:
--- a/mercurial/obsutil.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/obsutil.py	Thu Feb 15 17:18:26 2018 +0100
@@ -15,6 +15,7 @@
     phases,
     util,
 )
+from .utils import dateutil
 
 class marker(object):
     """Wrap obsolete marker raw data"""
@@ -841,11 +842,11 @@
         max_date = max(dates)
 
         if min_date == max_date:
-            fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
+            fmtmin_date = dateutil.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
             line.append(" (at %s)" % fmtmin_date)
         else:
-            fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
-            fmtmax_date = util.datestr(max_date, '%Y-%m-%d %H:%M %1%2')
+            fmtmin_date = dateutil.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
+            fmtmax_date = dateutil.datestr(max_date, '%Y-%m-%d %H:%M %1%2')
             line.append(" (between %s and %s)" % (fmtmin_date, fmtmax_date))
 
     return "".join(line)
--- a/mercurial/patch.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/patch.py	Thu Feb 15 17:18:26 2018 +0100
@@ -40,6 +40,7 @@
     util,
     vfs as vfsmod,
 )
+from .utils import dateutil
 
 diffhelpers = policy.importmod(r'diffhelpers')
 stringio = util.stringio
@@ -2669,8 +2670,8 @@
     def isempty(fctx):
         return fctx is None or fctx.size() == 0
 
-    date1 = util.datestr(ctx1.date())
-    date2 = util.datestr(ctx2.date())
+    date1 = dateutil.datestr(ctx1.date())
+    date2 = dateutil.datestr(ctx2.date())
 
     gitmode = {'l': '120000', 'x': '100755', '': '100644'}
 
--- a/mercurial/revset.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/revset.py	Thu Feb 15 17:18:26 2018 +0100
@@ -30,6 +30,7 @@
     smartset,
     util,
 )
+from .utils import dateutil
 
 # helpers for processing parsed tree
 getsymbol = revsetlang.getsymbol
@@ -658,7 +659,7 @@
     """
     # i18n: "date" is a keyword
     ds = getstring(x, _("date requires a string"))
-    dm = util.matchdate(ds)
+    dm = dateutil.matchdate(ds)
     return subset.filter(lambda x: dm(repo[x].date()[0]),
                          condrepr=('<date %r>', ds))
 
--- a/mercurial/subrepo.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/subrepo.py	Thu Feb 15 17:18:26 2018 +0100
@@ -36,6 +36,7 @@
     util,
     vfs as vfsmod,
 )
+from .utils import dateutil
 
 hg = None
 reporelpath = subrepoutil.reporelpath
@@ -1467,7 +1468,7 @@
         if date:
             # git's date parser silently ignores when seconds < 1e9
             # convert to ISO8601
-            env['GIT_AUTHOR_DATE'] = util.datestr(date,
+            env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
                                                   '%Y-%m-%dT%H:%M:%S %1%2')
         self._gitcommand(cmd, env=env)
         # make sure commit works otherwise HEAD might not exist under certain
--- a/mercurial/templatefilters.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/templatefilters.py	Thu Feb 15 17:18:26 2018 +0100
@@ -22,6 +22,7 @@
     url,
     util,
 )
+from .utils import dateutil
 
 urlerr = util.urlerr
 urlreq = util.urlreq
@@ -78,7 +79,7 @@
     else:
         delta = max(1, int(now - then))
         if delta > agescales[0][1] * 2:
-            return util.shortdate(date)
+            return dateutil.shortdate(date)
 
     for t, s, a in agescales:
         n = delta // s
@@ -203,7 +204,7 @@
     """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
     +0200".
     """
-    return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
+    return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
 
 @templatefilter('isodatesec')
 def isodatesec(text):
@@ -211,7 +212,7 @@
     seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
     filter.
     """
-    return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
+    return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
 
 def indent(text, prefix):
     '''indent each non-empty line of text after first with prefix.'''
@@ -325,14 +326,14 @@
     """Date. Returns a date using the Internet date format
     specified in RFC 3339: "2009-08-18T13:00:13+02:00".
     """
-    return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
+    return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
 
 @templatefilter('rfc822date')
 def rfc822date(text):
     """Date. Returns a date using the same format used in email
     headers: "Tue, 18 Aug 2009 13:00:13 +0200".
     """
-    return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
+    return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
 
 @templatefilter('short')
 def short(text):
@@ -353,7 +354,7 @@
 @templatefilter('shortdate')
 def shortdate(text):
     """Date. Returns a date like "2006-09-18"."""
-    return util.shortdate(text)
+    return dateutil.shortdate(text)
 
 @templatefilter('slashpath')
 def slashpath(path):
--- a/mercurial/templater.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/templater.py	Thu Feb 15 17:18:26 2018 +0100
@@ -29,6 +29,7 @@
     templatekw,
     util,
 )
+from .utils import dateutil
 
 class ResourceUnavailable(error.Abort):
     pass
@@ -649,9 +650,9 @@
         fmt = evalstring(context, mapping, args[1])
     try:
         if fmt is None:
-            return util.datestr(date)
+            return dateutil.datestr(date)
         else:
-            return util.datestr(date, fmt)
+            return dateutil.datestr(date, fmt)
     except (TypeError, ValueError):
         # i18n: "date" is a keyword
         raise error.ParseError(_("date expects a date information"))
@@ -954,7 +955,7 @@
 
     date = evalfuncarg(context, mapping, args[0])
     try:
-        date = util.parsedate(date)
+        date = dateutil.parsedate(date)
     except AttributeError:  # not str nor date tuple
         # i18n: "localdate" is a keyword
         raise error.ParseError(_("localdate expects a date information"))
@@ -962,7 +963,7 @@
         tzoffset = None
         tz = evalfuncarg(context, mapping, args[1])
         if isinstance(tz, bytes):
-            tzoffset, remainder = util.parsetimezone(tz)
+            tzoffset, remainder = dateutil.parsetimezone(tz)
             if remainder:
                 tzoffset = None
         if tzoffset is None:
@@ -972,7 +973,7 @@
                 # i18n: "localdate" is a keyword
                 raise error.ParseError(_("localdate expects a timezone"))
     else:
-        tzoffset = util.makedate()[1]
+        tzoffset = dateutil.makedate()[1]
     return (date[0], tzoffset)
 
 @templatefunc('max(iterable)')
--- a/mercurial/ui.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/ui.py	Thu Feb 15 17:18:26 2018 +0100
@@ -37,6 +37,7 @@
     scmutil,
     util,
 )
+from .utils import dateutil
 
 urlreq = util.urlreq
 
@@ -714,7 +715,7 @@
         (0, 0)
         """
         if self.config(section, name, default, untrusted):
-            return self.configwith(util.parsedate, section, name, default,
+            return self.configwith(dateutil.parsedate, section, name, default,
                                    'date', untrusted)
         if default is _unset:
             return None
--- a/mercurial/util.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/mercurial/util.py	Thu Feb 15 17:18:26 2018 +0100
@@ -17,11 +17,9 @@
 
 import abc
 import bz2
-import calendar
 import codecs
 import collections
 import contextlib
-import datetime
 import errno
 import gc
 import hashlib
@@ -55,6 +53,7 @@
     pycompat,
     urllibcompat,
 )
+from .utils import dateutil
 
 base85 = policy.importmod(r'base85')
 osutil = policy.importmod(r'osutil')
@@ -855,48 +854,6 @@
     if n == 4:
         return (vints[0], vints[1], vints[2], extra)
 
-# used by parsedate
-defaultdateformats = (
-    '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
-    '%Y-%m-%dT%H:%M',    #   without seconds
-    '%Y-%m-%dT%H%M%S',   # another awful but legal variant without :
-    '%Y-%m-%dT%H%M',     #   without seconds
-    '%Y-%m-%d %H:%M:%S', # our common legal variant
-    '%Y-%m-%d %H:%M',    #   without seconds
-    '%Y-%m-%d %H%M%S',   # without :
-    '%Y-%m-%d %H%M',     #   without seconds
-    '%Y-%m-%d %I:%M:%S%p',
-    '%Y-%m-%d %H:%M',
-    '%Y-%m-%d %I:%M%p',
-    '%Y-%m-%d',
-    '%m-%d',
-    '%m/%d',
-    '%m/%d/%y',
-    '%m/%d/%Y',
-    '%a %b %d %H:%M:%S %Y',
-    '%a %b %d %I:%M:%S%p %Y',
-    '%a, %d %b %Y %H:%M:%S',        #  GNU coreutils "/bin/date --rfc-2822"
-    '%b %d %H:%M:%S %Y',
-    '%b %d %I:%M:%S%p %Y',
-    '%b %d %H:%M:%S',
-    '%b %d %I:%M:%S%p',
-    '%b %d %H:%M',
-    '%b %d %I:%M%p',
-    '%b %d %Y',
-    '%b %d',
-    '%H:%M:%S',
-    '%I:%M:%S%p',
-    '%H:%M',
-    '%I:%M%p',
-)
-
-extendeddateformats = defaultdateformats + (
-    "%Y",
-    "%Y-%m",
-    "%b",
-    "%b %Y",
-    )
-
 def cachefunc(func):
     '''cache the result of function calls'''
     # XXX doesn't handle keywords args
@@ -2304,277 +2261,6 @@
 
         return data
 
-def makedate(timestamp=None):
-    '''Return a unix timestamp (or the current time) as a (unixtime,
-    offset) tuple based off the local timezone.'''
-    if timestamp is None:
-        timestamp = time.time()
-    if timestamp < 0:
-        hint = _("check your clock")
-        raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
-    delta = (datetime.datetime.utcfromtimestamp(timestamp) -
-             datetime.datetime.fromtimestamp(timestamp))
-    tz = delta.days * 86400 + delta.seconds
-    return timestamp, tz
-
-def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
-    """represent a (unixtime, offset) tuple as a localized time.
-    unixtime is seconds since the epoch, and offset is the time zone's
-    number of seconds away from UTC.
-
-    >>> datestr((0, 0))
-    'Thu Jan 01 00:00:00 1970 +0000'
-    >>> datestr((42, 0))
-    'Thu Jan 01 00:00:42 1970 +0000'
-    >>> datestr((-42, 0))
-    'Wed Dec 31 23:59:18 1969 +0000'
-    >>> datestr((0x7fffffff, 0))
-    'Tue Jan 19 03:14:07 2038 +0000'
-    >>> datestr((-0x80000000, 0))
-    'Fri Dec 13 20:45:52 1901 +0000'
-    """
-    t, tz = date or makedate()
-    if "%1" in format or "%2" in format or "%z" in format:
-        sign = (tz > 0) and "-" or "+"
-        minutes = abs(tz) // 60
-        q, r = divmod(minutes, 60)
-        format = format.replace("%z", "%1%2")
-        format = format.replace("%1", "%c%02d" % (sign, q))
-        format = format.replace("%2", "%02d" % r)
-    d = t - tz
-    if d > 0x7fffffff:
-        d = 0x7fffffff
-    elif d < -0x80000000:
-        d = -0x80000000
-    # Never use time.gmtime() and datetime.datetime.fromtimestamp()
-    # because they use the gmtime() system call which is buggy on Windows
-    # for negative values.
-    t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
-    s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
-    return s
-
-def shortdate(date=None):
-    """turn (timestamp, tzoff) tuple into iso 8631 date."""
-    return datestr(date, format='%Y-%m-%d')
-
-def parsetimezone(s):
-    """find a trailing timezone, if any, in string, and return a
-       (offset, remainder) pair"""
-    s = pycompat.bytestr(s)
-
-    if s.endswith("GMT") or s.endswith("UTC"):
-        return 0, s[:-3].rstrip()
-
-    # Unix-style timezones [+-]hhmm
-    if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
-        sign = (s[-5] == "+") and 1 or -1
-        hours = int(s[-4:-2])
-        minutes = int(s[-2:])
-        return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
-
-    # ISO8601 trailing Z
-    if s.endswith("Z") and s[-2:-1].isdigit():
-        return 0, s[:-1]
-
-    # ISO8601-style [+-]hh:mm
-    if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
-        s[-5:-3].isdigit() and s[-2:].isdigit()):
-        sign = (s[-6] == "+") and 1 or -1
-        hours = int(s[-5:-3])
-        minutes = int(s[-2:])
-        return -sign * (hours * 60 + minutes) * 60, s[:-6]
-
-    return None, s
-
-def strdate(string, format, defaults=None):
-    """parse a localized time string and return a (unixtime, offset) tuple.
-    if the string cannot be parsed, ValueError is raised."""
-    if defaults is None:
-        defaults = {}
-
-    # NOTE: unixtime = localunixtime + offset
-    offset, date = parsetimezone(string)
-
-    # add missing elements from defaults
-    usenow = False # default to using biased defaults
-    for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
-        part = pycompat.bytestr(part)
-        found = [True for p in part if ("%"+p) in format]
-        if not found:
-            date += "@" + defaults[part][usenow]
-            format += "@%" + part[0]
-        else:
-            # We've found a specific time element, less specific time
-            # elements are relative to today
-            usenow = True
-
-    timetuple = time.strptime(encoding.strfromlocal(date),
-                              encoding.strfromlocal(format))
-    localunixtime = int(calendar.timegm(timetuple))
-    if offset is None:
-        # local timezone
-        unixtime = int(time.mktime(timetuple))
-        offset = unixtime - localunixtime
-    else:
-        unixtime = localunixtime + offset
-    return unixtime, offset
-
-def parsedate(date, formats=None, bias=None):
-    """parse a localized date/time and return a (unixtime, offset) tuple.
-
-    The date may be a "unixtime offset" string or in one of the specified
-    formats. If the date already is a (unixtime, offset) tuple, it is returned.
-
-    >>> parsedate(b' today ') == parsedate(
-    ...     datetime.date.today().strftime('%b %d').encode('ascii'))
-    True
-    >>> parsedate(b'yesterday ') == parsedate(
-    ...     (datetime.date.today() - datetime.timedelta(days=1)
-    ...      ).strftime('%b %d').encode('ascii'))
-    True
-    >>> now, tz = makedate()
-    >>> strnow, strtz = parsedate(b'now')
-    >>> (strnow - now) < 1
-    True
-    >>> tz == strtz
-    True
-    """
-    if bias is None:
-        bias = {}
-    if not date:
-        return 0, 0
-    if isinstance(date, tuple) and len(date) == 2:
-        return date
-    if not formats:
-        formats = defaultdateformats
-    date = date.strip()
-
-    if date == 'now' or date == _('now'):
-        return makedate()
-    if date == 'today' or date == _('today'):
-        date = datetime.date.today().strftime(r'%b %d')
-        date = encoding.strtolocal(date)
-    elif date == 'yesterday' or date == _('yesterday'):
-        date = (datetime.date.today() -
-                datetime.timedelta(days=1)).strftime(r'%b %d')
-        date = encoding.strtolocal(date)
-
-    try:
-        when, offset = map(int, date.split(' '))
-    except ValueError:
-        # fill out defaults
-        now = makedate()
-        defaults = {}
-        for part in ("d", "mb", "yY", "HI", "M", "S"):
-            # this piece is for rounding the specific end of unknowns
-            b = bias.get(part)
-            if b is None:
-                if part[0:1] in "HMS":
-                    b = "00"
-                else:
-                    b = "0"
-
-            # this piece is for matching the generic end to today's date
-            n = datestr(now, "%" + part[0:1])
-
-            defaults[part] = (b, n)
-
-        for format in formats:
-            try:
-                when, offset = strdate(date, format, defaults)
-            except (ValueError, OverflowError):
-                pass
-            else:
-                break
-        else:
-            raise error.ParseError(
-                _('invalid date: %r') % pycompat.bytestr(date))
-    # validate explicit (probably user-specified) date and
-    # time zone offset. values must fit in signed 32 bits for
-    # current 32-bit linux runtimes. timezones go from UTC-12
-    # to UTC+14
-    if when < -0x80000000 or when > 0x7fffffff:
-        raise error.ParseError(_('date exceeds 32 bits: %d') % when)
-    if offset < -50400 or offset > 43200:
-        raise error.ParseError(_('impossible time zone offset: %d') % offset)
-    return when, offset
-
-def matchdate(date):
-    """Return a function that matches a given date match specifier
-
-    Formats include:
-
-    '{date}' match a given date to the accuracy provided
-
-    '<{date}' on or before a given date
-
-    '>{date}' on or after a given date
-
-    >>> p1 = parsedate(b"10:29:59")
-    >>> p2 = parsedate(b"10:30:00")
-    >>> p3 = parsedate(b"10:30:59")
-    >>> p4 = parsedate(b"10:31:00")
-    >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
-    >>> f = matchdate(b"10:30")
-    >>> f(p1[0])
-    False
-    >>> f(p2[0])
-    True
-    >>> f(p3[0])
-    True
-    >>> f(p4[0])
-    False
-    >>> f(p5[0])
-    False
-    """
-
-    def lower(date):
-        d = {'mb': "1", 'd': "1"}
-        return parsedate(date, extendeddateformats, d)[0]
-
-    def upper(date):
-        d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
-        for days in ("31", "30", "29"):
-            try:
-                d["d"] = days
-                return parsedate(date, extendeddateformats, d)[0]
-            except error.ParseError:
-                pass
-        d["d"] = "28"
-        return parsedate(date, extendeddateformats, d)[0]
-
-    date = date.strip()
-
-    if not date:
-        raise Abort(_("dates cannot consist entirely of whitespace"))
-    elif date[0] == "<":
-        if not date[1:]:
-            raise Abort(_("invalid day spec, use '<DATE'"))
-        when = upper(date[1:])
-        return lambda x: x <= when
-    elif date[0] == ">":
-        if not date[1:]:
-            raise Abort(_("invalid day spec, use '>DATE'"))
-        when = lower(date[1:])
-        return lambda x: x >= when
-    elif date[0] == "-":
-        try:
-            days = int(date[1:])
-        except ValueError:
-            raise Abort(_("invalid day spec: %s") % date[1:])
-        if days < 0:
-            raise Abort(_("%s must be nonnegative (see 'hg help dates')")
-                % date[1:])
-        when = makedate()[0] - days * 3600 * 24
-        return lambda x: x >= when
-    elif " to " in date:
-        a, b = date.split(" to ")
-        start, stop = lower(a), upper(b)
-        return lambda x: x >= start and x <= stop
-    else:
-        start, stop = lower(date), upper(date)
-        return lambda x: x >= start and x <= stop
-
 def stringmatcher(pattern, casesensitive=True):
     """
     accepts a string, possibly starting with 're:' or 'literal:' prefix.
@@ -4303,3 +3989,54 @@
         if not (byte & 0x80):
             return result
         shift += 7
+
+###
+# Deprecation warnings for util.py splitting
+###
+
+defaultdateformats = dateutil.defaultdateformats
+
+extendeddateformats = dateutil.extendeddateformats
+
+def makedate(*args, **kwargs):
+    msg = ("'util.makedate' is deprecated, "
+           "use 'utils.dateutil.makedate'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.makedate(*args, **kwargs)
+
+def datestr(*args, **kwargs):
+    msg = ("'util.datestr' is deprecated, "
+           "use 'utils.dateutil.datestr'")
+    nouideprecwarn(msg, "4.6")
+    debugstacktrace()
+    return dateutil.datestr(*args, **kwargs)
+
+def shortdate(*args, **kwargs):
+    msg = ("'util.shortdate' is deprecated, "
+           "use 'utils.dateutil.shortdate'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.shortdate(*args, **kwargs)
+
+def parsetimezone(*args, **kwargs):
+    msg = ("'util.parsetimezone' is deprecated, "
+           "use 'utils.dateutil.parsetimezone'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.parsetimezone(*args, **kwargs)
+
+def strdate(*args, **kwargs):
+    msg = ("'util.strdate' is deprecated, "
+           "use 'utils.dateutil.strdate'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.strdate(*args, **kwargs)
+
+def parsedate(*args, **kwargs):
+    msg = ("'util.parsedate' is deprecated, "
+           "use 'utils.dateutil.parsedate'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.parsedate(*args, **kwargs)
+
+def matchdate(*args, **kwargs):
+    msg = ("'util.matchdate' is deprecated, "
+           "use 'utils.dateutil.matchdate'")
+    nouideprecwarn(msg, "4.6")
+    return dateutil.matchdate(*args, **kwargs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/utils/dateutil.py	Thu Feb 15 17:18:26 2018 +0100
@@ -0,0 +1,332 @@
+# util.py - Mercurial utility functions relative to dates
+#
+#  Copyright 2018 Boris Feld <boris.feld@octobus.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import, print_function
+
+import calendar
+import datetime
+import time
+
+from ..i18n import _
+from .. import (
+    encoding,
+    error,
+    pycompat,
+)
+
+# used by parsedate
+defaultdateformats = (
+    '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
+    '%Y-%m-%dT%H:%M',    #   without seconds
+    '%Y-%m-%dT%H%M%S',   # another awful but legal variant without :
+    '%Y-%m-%dT%H%M',     #   without seconds
+    '%Y-%m-%d %H:%M:%S', # our common legal variant
+    '%Y-%m-%d %H:%M',    #   without seconds
+    '%Y-%m-%d %H%M%S',   # without :
+    '%Y-%m-%d %H%M',     #   without seconds
+    '%Y-%m-%d %I:%M:%S%p',
+    '%Y-%m-%d %H:%M',
+    '%Y-%m-%d %I:%M%p',
+    '%Y-%m-%d',
+    '%m-%d',
+    '%m/%d',
+    '%m/%d/%y',
+    '%m/%d/%Y',
+    '%a %b %d %H:%M:%S %Y',
+    '%a %b %d %I:%M:%S%p %Y',
+    '%a, %d %b %Y %H:%M:%S',        #  GNU coreutils "/bin/date --rfc-2822"
+    '%b %d %H:%M:%S %Y',
+    '%b %d %I:%M:%S%p %Y',
+    '%b %d %H:%M:%S',
+    '%b %d %I:%M:%S%p',
+    '%b %d %H:%M',
+    '%b %d %I:%M%p',
+    '%b %d %Y',
+    '%b %d',
+    '%H:%M:%S',
+    '%I:%M:%S%p',
+    '%H:%M',
+    '%I:%M%p',
+)
+
+extendeddateformats = defaultdateformats + (
+    "%Y",
+    "%Y-%m",
+    "%b",
+    "%b %Y",
+)
+
+def makedate(timestamp=None):
+    '''Return a unix timestamp (or the current time) as a (unixtime,
+    offset) tuple based off the local timezone.'''
+    if timestamp is None:
+        timestamp = time.time()
+    if timestamp < 0:
+        hint = _("check your clock")
+        raise error.Abort(_("negative timestamp: %d") % timestamp, hint=hint)
+    delta = (datetime.datetime.utcfromtimestamp(timestamp) -
+             datetime.datetime.fromtimestamp(timestamp))
+    tz = delta.days * 86400 + delta.seconds
+    return timestamp, tz
+
+def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
+    """represent a (unixtime, offset) tuple as a localized time.
+    unixtime is seconds since the epoch, and offset is the time zone's
+    number of seconds away from UTC.
+
+    >>> datestr((0, 0))
+    'Thu Jan 01 00:00:00 1970 +0000'
+    >>> datestr((42, 0))
+    'Thu Jan 01 00:00:42 1970 +0000'
+    >>> datestr((-42, 0))
+    'Wed Dec 31 23:59:18 1969 +0000'
+    >>> datestr((0x7fffffff, 0))
+    'Tue Jan 19 03:14:07 2038 +0000'
+    >>> datestr((-0x80000000, 0))
+    'Fri Dec 13 20:45:52 1901 +0000'
+    """
+    t, tz = date or makedate()
+    if "%1" in format or "%2" in format or "%z" in format:
+        sign = (tz > 0) and "-" or "+"
+        minutes = abs(tz) // 60
+        q, r = divmod(minutes, 60)
+        format = format.replace("%z", "%1%2")
+        format = format.replace("%1", "%c%02d" % (sign, q))
+        format = format.replace("%2", "%02d" % r)
+    d = t - tz
+    if d > 0x7fffffff:
+        d = 0x7fffffff
+    elif d < -0x80000000:
+        d = -0x80000000
+    # Never use time.gmtime() and datetime.datetime.fromtimestamp()
+    # because they use the gmtime() system call which is buggy on Windows
+    # for negative values.
+    t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
+    s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
+    return s
+
+def shortdate(date=None):
+    """turn (timestamp, tzoff) tuple into iso 8631 date."""
+    return datestr(date, format='%Y-%m-%d')
+
+def parsetimezone(s):
+    """find a trailing timezone, if any, in string, and return a
+       (offset, remainder) pair"""
+    s = pycompat.bytestr(s)
+
+    if s.endswith("GMT") or s.endswith("UTC"):
+        return 0, s[:-3].rstrip()
+
+    # Unix-style timezones [+-]hhmm
+    if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
+        sign = (s[-5] == "+") and 1 or -1
+        hours = int(s[-4:-2])
+        minutes = int(s[-2:])
+        return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
+
+    # ISO8601 trailing Z
+    if s.endswith("Z") and s[-2:-1].isdigit():
+        return 0, s[:-1]
+
+    # ISO8601-style [+-]hh:mm
+    if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
+        s[-5:-3].isdigit() and s[-2:].isdigit()):
+        sign = (s[-6] == "+") and 1 or -1
+        hours = int(s[-5:-3])
+        minutes = int(s[-2:])
+        return -sign * (hours * 60 + minutes) * 60, s[:-6]
+
+    return None, s
+
+def strdate(string, format, defaults=None):
+    """parse a localized time string and return a (unixtime, offset) tuple.
+    if the string cannot be parsed, ValueError is raised."""
+    if defaults is None:
+        defaults = {}
+
+    # NOTE: unixtime = localunixtime + offset
+    offset, date = parsetimezone(string)
+
+    # add missing elements from defaults
+    usenow = False # default to using biased defaults
+    for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
+        part = pycompat.bytestr(part)
+        found = [True for p in part if ("%"+p) in format]
+        if not found:
+            date += "@" + defaults[part][usenow]
+            format += "@%" + part[0]
+        else:
+            # We've found a specific time element, less specific time
+            # elements are relative to today
+            usenow = True
+
+    timetuple = time.strptime(encoding.strfromlocal(date),
+                              encoding.strfromlocal(format))
+    localunixtime = int(calendar.timegm(timetuple))
+    if offset is None:
+        # local timezone
+        unixtime = int(time.mktime(timetuple))
+        offset = unixtime - localunixtime
+    else:
+        unixtime = localunixtime + offset
+    return unixtime, offset
+
+def parsedate(date, formats=None, bias=None):
+    """parse a localized date/time and return a (unixtime, offset) tuple.
+
+    The date may be a "unixtime offset" string or in one of the specified
+    formats. If the date already is a (unixtime, offset) tuple, it is returned.
+
+    >>> parsedate(b' today ') == parsedate(
+    ...     datetime.date.today().strftime('%b %d').encode('ascii'))
+    True
+    >>> parsedate(b'yesterday ') == parsedate(
+    ...     (datetime.date.today() - datetime.timedelta(days=1)
+    ...      ).strftime('%b %d').encode('ascii'))
+    True
+    >>> now, tz = makedate()
+    >>> strnow, strtz = parsedate(b'now')
+    >>> (strnow - now) < 1
+    True
+    >>> tz == strtz
+    True
+    """
+    if bias is None:
+        bias = {}
+    if not date:
+        return 0, 0
+    if isinstance(date, tuple) and len(date) == 2:
+        return date
+    if not formats:
+        formats = defaultdateformats
+    date = date.strip()
+
+    if date == 'now' or date == _('now'):
+        return makedate()
+    if date == 'today' or date == _('today'):
+        date = datetime.date.today().strftime(r'%b %d')
+        date = encoding.strtolocal(date)
+    elif date == 'yesterday' or date == _('yesterday'):
+        date = (datetime.date.today() -
+                datetime.timedelta(days=1)).strftime(r'%b %d')
+        date = encoding.strtolocal(date)
+
+    try:
+        when, offset = map(int, date.split(' '))
+    except ValueError:
+        # fill out defaults
+        now = makedate()
+        defaults = {}
+        for part in ("d", "mb", "yY", "HI", "M", "S"):
+            # this piece is for rounding the specific end of unknowns
+            b = bias.get(part)
+            if b is None:
+                if part[0:1] in "HMS":
+                    b = "00"
+                else:
+                    b = "0"
+
+            # this piece is for matching the generic end to today's date
+            n = datestr(now, "%" + part[0:1])
+
+            defaults[part] = (b, n)
+
+        for format in formats:
+            try:
+                when, offset = strdate(date, format, defaults)
+            except (ValueError, OverflowError):
+                pass
+            else:
+                break
+        else:
+            raise error.ParseError(
+                _('invalid date: %r') % pycompat.bytestr(date))
+    # validate explicit (probably user-specified) date and
+    # time zone offset. values must fit in signed 32 bits for
+    # current 32-bit linux runtimes. timezones go from UTC-12
+    # to UTC+14
+    if when < -0x80000000 or when > 0x7fffffff:
+        raise error.ParseError(_('date exceeds 32 bits: %d') % when)
+    if offset < -50400 or offset > 43200:
+        raise error.ParseError(_('impossible time zone offset: %d') % offset)
+    return when, offset
+
+def matchdate(date):
+    """Return a function that matches a given date match specifier
+
+    Formats include:
+
+    '{date}' match a given date to the accuracy provided
+
+    '<{date}' on or before a given date
+
+    '>{date}' on or after a given date
+
+    >>> p1 = parsedate(b"10:29:59")
+    >>> p2 = parsedate(b"10:30:00")
+    >>> p3 = parsedate(b"10:30:59")
+    >>> p4 = parsedate(b"10:31:00")
+    >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
+    >>> f = matchdate(b"10:30")
+    >>> f(p1[0])
+    False
+    >>> f(p2[0])
+    True
+    >>> f(p3[0])
+    True
+    >>> f(p4[0])
+    False
+    >>> f(p5[0])
+    False
+    """
+
+    def lower(date):
+        d = {'mb': "1", 'd': "1"}
+        return parsedate(date, extendeddateformats, d)[0]
+
+    def upper(date):
+        d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
+        for days in ("31", "30", "29"):
+            try:
+                d["d"] = days
+                return parsedate(date, extendeddateformats, d)[0]
+            except error.ParseError:
+                pass
+        d["d"] = "28"
+        return parsedate(date, extendeddateformats, d)[0]
+
+    date = date.strip()
+
+    if not date:
+        raise error.Abort(_("dates cannot consist entirely of whitespace"))
+    elif date[0] == "<":
+        if not date[1:]:
+            raise error.Abort(_("invalid day spec, use '<DATE'"))
+        when = upper(date[1:])
+        return lambda x: x <= when
+    elif date[0] == ">":
+        if not date[1:]:
+            raise error.Abort(_("invalid day spec, use '>DATE'"))
+        when = lower(date[1:])
+        return lambda x: x >= when
+    elif date[0] == "-":
+        try:
+            days = int(date[1:])
+        except ValueError:
+            raise error.Abort(_("invalid day spec: %s") % date[1:])
+        if days < 0:
+            raise error.Abort(_("%s must be nonnegative (see 'hg help dates')")
+                % date[1:])
+        when = makedate()[0] - days * 3600 * 24
+        return lambda x: x >= when
+    elif " to " in date:
+        a, b = date.split(" to ")
+        start, stop = lower(a), upper(b)
+        return lambda x: x >= start and x <= stop
+    else:
+        start, stop = lower(date), upper(date)
+        return lambda x: x >= start and x <= stop
--- a/setup.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/setup.py	Thu Feb 15 17:18:26 2018 +0100
@@ -809,6 +809,7 @@
             'mercurial.pure',
             'mercurial.thirdparty',
             'mercurial.thirdparty.attr',
+            'mercurial.utils',
             'hgext', 'hgext.convert', 'hgext.fsmonitor',
             'hgext.fsmonitor.pywatchman', 'hgext.highlight',
             'hgext.largefiles', 'hgext.lfs', 'hgext.narrow',
--- a/tests/fakedirstatewritetime.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/tests/fakedirstatewritetime.py	Thu Feb 15 17:18:26 2018 +0100
@@ -13,8 +13,8 @@
     extensions,
     policy,
     registrar,
-    util,
 )
+from mercurial.utils import dateutil
 
 configtable = {}
 configitem = registrar.configitem(configtable)
@@ -49,7 +49,7 @@
 
     # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between
     # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy
-    fakenow = util.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
+    fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
 
     orig_pack_dirstate = parsers.pack_dirstate
     orig_dirstate_getfsnow = dirstate._getfsnow
--- a/tests/fakepatchtime.py	Thu Feb 08 23:27:24 2018 +0530
+++ b/tests/fakepatchtime.py	Thu Feb 15 17:18:26 2018 +0100
@@ -7,8 +7,8 @@
     extensions,
     patch as patchmod,
     registrar,
-    util,
 )
+from mercurial.utils import dateutil
 
 configtable = {}
 configitem = registrar.configitem(configtable)
@@ -30,7 +30,7 @@
     if fakenow:
         # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between
         # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy
-        fakenow = util.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
+        fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
         for f in files:
             repo.wvfs.utime(f, (fakenow, fakenow))
 
--- a/tests/test-journal.t	Thu Feb 08 23:27:24 2018 +0530
+++ b/tests/test-journal.t	Thu Feb 15 17:18:26 2018 +0100
@@ -4,6 +4,7 @@
   > # mock out util.getuser() and util.makedate() to supply testable values
   > import os
   > from mercurial import util
+  > from mercurial.utils import dateutil
   > def mockgetuser():
   >     return 'foobar'
   > 
@@ -19,7 +20,7 @@
   >     return (time, 0)
   > 
   > util.getuser = mockgetuser
-  > util.makedate = mockmakedate
+  > dateutil.makedate = mockmakedate
   > EOF
 
   $ cat >> $HGRCPATH << EOF