hgext/journal.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43104 74802979dd9d
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    47 
    47 
    48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    50 # be specifying the version(s) of Mercurial they are tested with, or
    50 # be specifying the version(s) of Mercurial they are tested with, or
    51 # leave the attribute unspecified.
    51 # leave the attribute unspecified.
    52 testedwith = 'ships-with-hg-core'
    52 testedwith = b'ships-with-hg-core'
    53 
    53 
    54 # storage format version; increment when the format changes
    54 # storage format version; increment when the format changes
    55 storageversion = 0
    55 storageversion = 0
    56 
    56 
    57 # namespaces
    57 # namespaces
    58 bookmarktype = 'bookmark'
    58 bookmarktype = b'bookmark'
    59 wdirparenttype = 'wdirparent'
    59 wdirparenttype = b'wdirparent'
    60 # In a shared repository, what shared feature name is used
    60 # In a shared repository, what shared feature name is used
    61 # to indicate this namespace is shared with the source?
    61 # to indicate this namespace is shared with the source?
    62 sharednamespaces = {
    62 sharednamespaces = {
    63     bookmarktype: hg.sharedbookmarks,
    63     bookmarktype: hg.sharedbookmarks,
    64 }
    64 }
    65 
    65 
    66 # Journal recording, register hooks and storage object
    66 # Journal recording, register hooks and storage object
    67 def extsetup(ui):
    67 def extsetup(ui):
    68     extensions.wrapfunction(dispatch, 'runcommand', runcommand)
    68     extensions.wrapfunction(dispatch, b'runcommand', runcommand)
    69     extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
    69     extensions.wrapfunction(bookmarks.bmstore, b'_write', recordbookmarks)
    70     extensions.wrapfilecache(
    70     extensions.wrapfilecache(
    71         localrepo.localrepository, 'dirstate', wrapdirstate
    71         localrepo.localrepository, b'dirstate', wrapdirstate
    72     )
    72     )
    73     extensions.wrapfunction(hg, 'postshare', wrappostshare)
    73     extensions.wrapfunction(hg, b'postshare', wrappostshare)
    74     extensions.wrapfunction(hg, 'copystore', unsharejournal)
    74     extensions.wrapfunction(hg, b'copystore', unsharejournal)
    75 
    75 
    76 
    76 
    77 def reposetup(ui, repo):
    77 def reposetup(ui, repo):
    78     if repo.local():
    78     if repo.local():
    79         repo.journal = journalstorage(repo)
    79         repo.journal = journalstorage(repo)
    80         repo._wlockfreeprefix.add('namejournal')
    80         repo._wlockfreeprefix.add(b'namejournal')
    81 
    81 
    82         dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
    82         dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
    83         if cached:
    83         if cached:
    84             # already instantiated dirstate isn't yet marked as
    84             # already instantiated dirstate isn't yet marked as
    85             # "journal"-ing, even though repo.dirstate() was already
    85             # "journal"-ing, even though repo.dirstate() was already
    86             # wrapped by own wrapdirstate()
    86             # wrapped by own wrapdirstate()
    87             _setupdirstate(repo, dirstate)
    87             _setupdirstate(repo, dirstate)
    93     return orig(lui, repo, cmd, fullargs, *args)
    93     return orig(lui, repo, cmd, fullargs, *args)
    94 
    94 
    95 
    95 
    96 def _setupdirstate(repo, dirstate):
    96 def _setupdirstate(repo, dirstate):
    97     dirstate.journalstorage = repo.journal
    97     dirstate.journalstorage = repo.journal
    98     dirstate.addparentchangecallback('journal', recorddirstateparents)
    98     dirstate.addparentchangecallback(b'journal', recorddirstateparents)
    99 
    99 
   100 
   100 
   101 # hooks to record dirstate changes
   101 # hooks to record dirstate changes
   102 def wrapdirstate(orig, repo):
   102 def wrapdirstate(orig, repo):
   103     """Make journal storage available to the dirstate object"""
   103     """Make journal storage available to the dirstate object"""
   104     dirstate = orig(repo)
   104     dirstate = orig(repo)
   105     if util.safehasattr(repo, 'journal'):
   105     if util.safehasattr(repo, b'journal'):
   106         _setupdirstate(repo, dirstate)
   106         _setupdirstate(repo, dirstate)
   107     return dirstate
   107     return dirstate
   108 
   108 
   109 
   109 
   110 def recorddirstateparents(dirstate, old, new):
   110 def recorddirstateparents(dirstate, old, new):
   111     """Records all dirstate parent changes in the journal."""
   111     """Records all dirstate parent changes in the journal."""
   112     old = list(old)
   112     old = list(old)
   113     new = list(new)
   113     new = list(new)
   114     if util.safehasattr(dirstate, 'journalstorage'):
   114     if util.safehasattr(dirstate, b'journalstorage'):
   115         # only record two hashes if there was a merge
   115         # only record two hashes if there was a merge
   116         oldhashes = old[:1] if old[1] == node.nullid else old
   116         oldhashes = old[:1] if old[1] == node.nullid else old
   117         newhashes = new[:1] if new[1] == node.nullid else new
   117         newhashes = new[:1] if new[1] == node.nullid else new
   118         dirstate.journalstorage.record(
   118         dirstate.journalstorage.record(
   119             wdirparenttype, '.', oldhashes, newhashes
   119             wdirparenttype, b'.', oldhashes, newhashes
   120         )
   120         )
   121 
   121 
   122 
   122 
   123 # hooks to record bookmark changes (both local and remote)
   123 # hooks to record bookmark changes (both local and remote)
   124 def recordbookmarks(orig, store, fp):
   124 def recordbookmarks(orig, store, fp):
   125     """Records all bookmark changes in the journal."""
   125     """Records all bookmark changes in the journal."""
   126     repo = store._repo
   126     repo = store._repo
   127     if util.safehasattr(repo, 'journal'):
   127     if util.safehasattr(repo, b'journal'):
   128         oldmarks = bookmarks.bmstore(repo)
   128         oldmarks = bookmarks.bmstore(repo)
   129         for mark, value in store.iteritems():
   129         for mark, value in store.iteritems():
   130             oldvalue = oldmarks.get(mark, node.nullid)
   130             oldvalue = oldmarks.get(mark, node.nullid)
   131             if value != oldvalue:
   131             if value != oldvalue:
   132                 repo.journal.record(bookmarktype, mark, oldvalue, value)
   132                 repo.journal.record(bookmarktype, mark, oldvalue, value)
   135 
   135 
   136 # shared repository support
   136 # shared repository support
   137 def _readsharedfeatures(repo):
   137 def _readsharedfeatures(repo):
   138     """A set of shared features for this repository"""
   138     """A set of shared features for this repository"""
   139     try:
   139     try:
   140         return set(repo.vfs.read('shared').splitlines())
   140         return set(repo.vfs.read(b'shared').splitlines())
   141     except IOError as inst:
   141     except IOError as inst:
   142         if inst.errno != errno.ENOENT:
   142         if inst.errno != errno.ENOENT:
   143             raise
   143             raise
   144         return set()
   144         return set()
   145 
   145 
   175 
   175 
   176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
   176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
   177     """Mark this shared working copy as sharing journal information"""
   177     """Mark this shared working copy as sharing journal information"""
   178     with destrepo.wlock():
   178     with destrepo.wlock():
   179         orig(sourcerepo, destrepo, **kwargs)
   179         orig(sourcerepo, destrepo, **kwargs)
   180         with destrepo.vfs('shared', 'a') as fp:
   180         with destrepo.vfs(b'shared', b'a') as fp:
   181             fp.write('journal\n')
   181             fp.write(b'journal\n')
   182 
   182 
   183 
   183 
   184 def unsharejournal(orig, ui, repo, repopath):
   184 def unsharejournal(orig, ui, repo, repopath):
   185     """Copy shared journal entries into this repo when unsharing"""
   185     """Copy shared journal entries into this repo when unsharing"""
   186     if (
   186     if (
   187         repo.path == repopath
   187         repo.path == repopath
   188         and repo.shared()
   188         and repo.shared()
   189         and util.safehasattr(repo, 'journal')
   189         and util.safehasattr(repo, b'journal')
   190     ):
   190     ):
   191         sharedrepo = hg.sharedreposource(repo)
   191         sharedrepo = hg.sharedreposource(repo)
   192         sharedfeatures = _readsharedfeatures(repo)
   192         sharedfeatures = _readsharedfeatures(repo)
   193         if sharedrepo and sharedfeatures > {'journal'}:
   193         if sharedrepo and sharedfeatures > {b'journal'}:
   194             # there is a shared repository and there are shared journal entries
   194             # there is a shared repository and there are shared journal entries
   195             # to copy. move shared date over from source to destination but
   195             # to copy. move shared date over from source to destination but
   196             # move the local file first
   196             # move the local file first
   197             if repo.vfs.exists('namejournal'):
   197             if repo.vfs.exists(b'namejournal'):
   198                 journalpath = repo.vfs.join('namejournal')
   198                 journalpath = repo.vfs.join(b'namejournal')
   199                 util.rename(journalpath, journalpath + '.bak')
   199                 util.rename(journalpath, journalpath + b'.bak')
   200             storage = repo.journal
   200             storage = repo.journal
   201             local = storage._open(
   201             local = storage._open(
   202                 repo.vfs, filename='namejournal.bak', _newestfirst=False
   202                 repo.vfs, filename=b'namejournal.bak', _newestfirst=False
   203             )
   203             )
   204             shared = (
   204             shared = (
   205                 e
   205                 e
   206                 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
   206                 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
   207                 if sharednamespaces.get(e.namespace) in sharedfeatures
   207                 if sharednamespaces.get(e.namespace) in sharedfeatures
   243             command,
   243             command,
   244             namespace,
   244             namespace,
   245             name,
   245             name,
   246             oldhashes,
   246             oldhashes,
   247             newhashes,
   247             newhashes,
   248         ) = line.split('\n')
   248         ) = line.split(b'\n')
   249         timestamp, tz = time.split()
   249         timestamp, tz = time.split()
   250         timestamp, tz = float(timestamp), int(tz)
   250         timestamp, tz = float(timestamp), int(tz)
   251         oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(','))
   251         oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(b','))
   252         newhashes = tuple(node.bin(hash) for hash in newhashes.split(','))
   252         newhashes = tuple(node.bin(hash) for hash in newhashes.split(b','))
   253         return cls(
   253         return cls(
   254             (timestamp, tz),
   254             (timestamp, tz),
   255             user,
   255             user,
   256             command,
   256             command,
   257             namespace,
   257             namespace,
   260             newhashes,
   260             newhashes,
   261         )
   261         )
   262 
   262 
   263     def __bytes__(self):
   263     def __bytes__(self):
   264         """bytes representation for storage"""
   264         """bytes representation for storage"""
   265         time = ' '.join(map(pycompat.bytestr, self.timestamp))
   265         time = b' '.join(map(pycompat.bytestr, self.timestamp))
   266         oldhashes = ','.join([node.hex(hash) for hash in self.oldhashes])
   266         oldhashes = b','.join([node.hex(hash) for hash in self.oldhashes])
   267         newhashes = ','.join([node.hex(hash) for hash in self.newhashes])
   267         newhashes = b','.join([node.hex(hash) for hash in self.newhashes])
   268         return '\n'.join(
   268         return b'\n'.join(
   269             (
   269             (
   270                 time,
   270                 time,
   271                 self.user,
   271                 self.user,
   272                 self.command,
   272                 self.command,
   273                 self.namespace,
   273                 self.namespace,
   309         # is this working copy using a shared storage?
   309         # is this working copy using a shared storage?
   310         self.sharedfeatures = self.sharedvfs = None
   310         self.sharedfeatures = self.sharedvfs = None
   311         if repo.shared():
   311         if repo.shared():
   312             features = _readsharedfeatures(repo)
   312             features = _readsharedfeatures(repo)
   313             sharedrepo = hg.sharedreposource(repo)
   313             sharedrepo = hg.sharedreposource(repo)
   314             if sharedrepo is not None and 'journal' in features:
   314             if sharedrepo is not None and b'journal' in features:
   315                 self.sharedvfs = sharedrepo.vfs
   315                 self.sharedvfs = sharedrepo.vfs
   316                 self.sharedfeatures = features
   316                 self.sharedfeatures = features
   317 
   317 
   318     # track the current command for recording in journal entries
   318     # track the current command for recording in journal entries
   319     @property
   319     @property
   320     def command(self):
   320     def command(self):
   321         commandstr = ' '.join(
   321         commandstr = b' '.join(
   322             map(procutil.shellquote, journalstorage._currentcommand)
   322             map(procutil.shellquote, journalstorage._currentcommand)
   323         )
   323         )
   324         if '\n' in commandstr:
   324         if b'\n' in commandstr:
   325             # truncate multi-line commands
   325             # truncate multi-line commands
   326             commandstr = commandstr.partition('\n')[0] + ' ...'
   326             commandstr = commandstr.partition(b'\n')[0] + b' ...'
   327         return commandstr
   327         return commandstr
   328 
   328 
   329     @classmethod
   329     @classmethod
   330     def recordcommand(cls, *fullargs):
   330     def recordcommand(cls, *fullargs):
   331         """Set the current hg arguments, stored with recorded entries"""
   331         """Set the current hg arguments, stored with recorded entries"""
   346         return l
   346         return l
   347 
   347 
   348     def jlock(self, vfs):
   348     def jlock(self, vfs):
   349         """Create a lock for the journal file"""
   349         """Create a lock for the journal file"""
   350         if self._currentlock(self._lockref) is not None:
   350         if self._currentlock(self._lockref) is not None:
   351             raise error.Abort(_('journal lock does not support nesting'))
   351             raise error.Abort(_(b'journal lock does not support nesting'))
   352         desc = _('journal of %s') % vfs.base
   352         desc = _(b'journal of %s') % vfs.base
   353         try:
   353         try:
   354             l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc)
   354             l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc)
   355         except error.LockHeld as inst:
   355         except error.LockHeld as inst:
   356             self.ui.warn(
   356             self.ui.warn(
   357                 _("waiting for lock on %s held by %r\n") % (desc, inst.locker)
   357                 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker)
   358             )
   358             )
   359             # default to 600 seconds timeout
   359             # default to 600 seconds timeout
   360             l = lock.lock(
   360             l = lock.lock(
   361                 vfs,
   361                 vfs,
   362                 'namejournal.lock',
   362                 b'namejournal.lock',
   363                 self.ui.configint("ui", "timeout"),
   363                 self.ui.configint(b"ui", b"timeout"),
   364                 desc=desc,
   364                 desc=desc,
   365             )
   365             )
   366             self.ui.warn(_("got lock after %s seconds\n") % l.delay)
   366             self.ui.warn(_(b"got lock after %s seconds\n") % l.delay)
   367         self._lockref = weakref.ref(l)
   367         self._lockref = weakref.ref(l)
   368         return l
   368         return l
   369 
   369 
   370     def record(self, namespace, name, oldhashes, newhashes):
   370     def record(self, namespace, name, oldhashes, newhashes):
   371         """Record a new journal entry
   371         """Record a new journal entry
   404         self._write(vfs, entry)
   404         self._write(vfs, entry)
   405 
   405 
   406     def _write(self, vfs, entry):
   406     def _write(self, vfs, entry):
   407         with self.jlock(vfs):
   407         with self.jlock(vfs):
   408             # open file in amend mode to ensure it is created if missing
   408             # open file in amend mode to ensure it is created if missing
   409             with vfs('namejournal', mode='a+b') as f:
   409             with vfs(b'namejournal', mode=b'a+b') as f:
   410                 f.seek(0, os.SEEK_SET)
   410                 f.seek(0, os.SEEK_SET)
   411                 # Read just enough bytes to get a version number (up to 2
   411                 # Read just enough bytes to get a version number (up to 2
   412                 # digits plus separator)
   412                 # digits plus separator)
   413                 version = f.read(3).partition('\0')[0]
   413                 version = f.read(3).partition(b'\0')[0]
   414                 if version and version != "%d" % storageversion:
   414                 if version and version != b"%d" % storageversion:
   415                     # different version of the storage. Exit early (and not
   415                     # different version of the storage. Exit early (and not
   416                     # write anything) if this is not a version we can handle or
   416                     # write anything) if this is not a version we can handle or
   417                     # the file is corrupt. In future, perhaps rotate the file
   417                     # the file is corrupt. In future, perhaps rotate the file
   418                     # instead?
   418                     # instead?
   419                     self.ui.warn(
   419                     self.ui.warn(
   420                         _("unsupported journal file version '%s'\n") % version
   420                         _(b"unsupported journal file version '%s'\n") % version
   421                     )
   421                     )
   422                     return
   422                     return
   423                 if not version:
   423                 if not version:
   424                     # empty file, write version first
   424                     # empty file, write version first
   425                     f.write(("%d" % storageversion) + '\0')
   425                     f.write((b"%d" % storageversion) + b'\0')
   426                 f.seek(0, os.SEEK_END)
   426                 f.seek(0, os.SEEK_END)
   427                 f.write(bytes(entry) + '\0')
   427                 f.write(bytes(entry) + b'\0')
   428 
   428 
   429     def filtered(self, namespace=None, name=None):
   429     def filtered(self, namespace=None, name=None):
   430         """Yield all journal entries with the given namespace or name
   430         """Yield all journal entries with the given namespace or name
   431 
   431 
   432         Both the namespace and the name are optional; if neither is given all
   432         Both the namespace and the name are optional; if neither is given all
   465             for e in self._open(self.sharedvfs)
   465             for e in self._open(self.sharedvfs)
   466             if sharednamespaces.get(e.namespace) in self.sharedfeatures
   466             if sharednamespaces.get(e.namespace) in self.sharedfeatures
   467         )
   467         )
   468         return _mergeentriesiter(local, shared)
   468         return _mergeentriesiter(local, shared)
   469 
   469 
   470     def _open(self, vfs, filename='namejournal', _newestfirst=True):
   470     def _open(self, vfs, filename=b'namejournal', _newestfirst=True):
   471         if not vfs.exists(filename):
   471         if not vfs.exists(filename):
   472             return
   472             return
   473 
   473 
   474         with vfs(filename) as f:
   474         with vfs(filename) as f:
   475             raw = f.read()
   475             raw = f.read()
   476 
   476 
   477         lines = raw.split('\0')
   477         lines = raw.split(b'\0')
   478         version = lines and lines[0]
   478         version = lines and lines[0]
   479         if version != "%d" % storageversion:
   479         if version != b"%d" % storageversion:
   480             version = version or _('not available')
   480             version = version or _(b'not available')
   481             raise error.Abort(_("unknown journal file version '%s'") % version)
   481             raise error.Abort(_(b"unknown journal file version '%s'") % version)
   482 
   482 
   483         # Skip the first line, it's a version number. Normally we iterate over
   483         # Skip the first line, it's a version number. Normally we iterate over
   484         # these in reverse order to list newest first; only when copying across
   484         # these in reverse order to list newest first; only when copying across
   485         # a shared storage do we forgo reversing.
   485         # a shared storage do we forgo reversing.
   486         lines = lines[1:]
   486         lines = lines[1:]
   492             yield journalentry.fromstorage(line)
   492             yield journalentry.fromstorage(line)
   493 
   493 
   494 
   494 
   495 # journal reading
   495 # journal reading
   496 # log options that don't make sense for journal
   496 # log options that don't make sense for journal
   497 _ignoreopts = ('no-merges', 'graph')
   497 _ignoreopts = (b'no-merges', b'graph')
   498 
   498 
   499 
   499 
   500 @command(
   500 @command(
   501     'journal',
   501     b'journal',
   502     [
   502     [
   503         ('', 'all', None, 'show history for all names'),
   503         (b'', b'all', None, b'show history for all names'),
   504         ('c', 'commits', None, 'show commit metadata'),
   504         (b'c', b'commits', None, b'show commit metadata'),
   505     ]
   505     ]
   506     + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
   506     + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
   507     '[OPTION]... [BOOKMARKNAME]',
   507     b'[OPTION]... [BOOKMARKNAME]',
   508     helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
   508     helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
   509 )
   509 )
   510 def journal(ui, repo, *args, **opts):
   510 def journal(ui, repo, *args, **opts):
   511     """show the previous position of bookmarks and the working copy
   511     """show the previous position of bookmarks and the working copy
   512 
   512 
   531 
   531 
   532     `hg journal -T json` can be used to produce machine readable output.
   532     `hg journal -T json` can be used to produce machine readable output.
   533 
   533 
   534     """
   534     """
   535     opts = pycompat.byteskwargs(opts)
   535     opts = pycompat.byteskwargs(opts)
   536     name = '.'
   536     name = b'.'
   537     if opts.get('all'):
   537     if opts.get(b'all'):
   538         if args:
   538         if args:
   539             raise error.Abort(
   539             raise error.Abort(
   540                 _("You can't combine --all and filtering on a name")
   540                 _(b"You can't combine --all and filtering on a name")
   541             )
   541             )
   542         name = None
   542         name = None
   543     if args:
   543     if args:
   544         name = args[0]
   544         name = args[0]
   545 
   545 
   546     fm = ui.formatter('journal', opts)
   546     fm = ui.formatter(b'journal', opts)
   547 
   547 
   548     def formatnodes(nodes):
   548     def formatnodes(nodes):
   549         return fm.formatlist(map(fm.hexfunc, nodes), name='node', sep=',')
   549         return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',')
   550 
   550 
   551     if opts.get("template") != "json":
   551     if opts.get(b"template") != b"json":
   552         if name is None:
   552         if name is None:
   553             displayname = _('the working copy and bookmarks')
   553             displayname = _(b'the working copy and bookmarks')
   554         else:
   554         else:
   555             displayname = "'%s'" % name
   555             displayname = b"'%s'" % name
   556         ui.status(_("previous locations of %s:\n") % displayname)
   556         ui.status(_(b"previous locations of %s:\n") % displayname)
   557 
   557 
   558     limit = logcmdutil.getlimit(opts)
   558     limit = logcmdutil.getlimit(opts)
   559     entry = None
   559     entry = None
   560     ui.pager('journal')
   560     ui.pager(b'journal')
   561     for count, entry in enumerate(repo.journal.filtered(name=name)):
   561     for count, entry in enumerate(repo.journal.filtered(name=name)):
   562         if count == limit:
   562         if count == limit:
   563             break
   563             break
   564 
   564 
   565         fm.startitem()
   565         fm.startitem()
   566         fm.condwrite(
   566         fm.condwrite(
   567             ui.verbose, 'oldnodes', '%s -> ', formatnodes(entry.oldhashes)
   567             ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes)
   568         )
   568         )
   569         fm.write('newnodes', '%s', formatnodes(entry.newhashes))
   569         fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
   570         fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user)
   570         fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
   571         fm.condwrite(
   571         fm.condwrite(
   572             opts.get('all') or name.startswith('re:'),
   572             opts.get(b'all') or name.startswith(b're:'),
   573             'name',
   573             b'name',
   574             '  %-8s',
   574             b'  %-8s',
   575             entry.name,
   575             entry.name,
   576         )
   576         )
   577 
   577 
   578         fm.condwrite(
   578         fm.condwrite(
   579             ui.verbose,
   579             ui.verbose,
   580             'date',
   580             b'date',
   581             ' %s',
   581             b' %s',
   582             fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2'),
   582             fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'),
   583         )
   583         )
   584         fm.write('command', '  %s\n', entry.command)
   584         fm.write(b'command', b'  %s\n', entry.command)
   585 
   585 
   586         if opts.get("commits"):
   586         if opts.get(b"commits"):
   587             if fm.isplain():
   587             if fm.isplain():
   588                 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
   588                 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
   589             else:
   589             else:
   590                 displayer = logcmdutil.changesetformatter(
   590                 displayer = logcmdutil.changesetformatter(
   591                     ui, repo, fm.nested('changesets'), diffopts=opts
   591                     ui, repo, fm.nested(b'changesets'), diffopts=opts
   592                 )
   592                 )
   593             for hash in entry.newhashes:
   593             for hash in entry.newhashes:
   594                 try:
   594                 try:
   595                     ctx = repo[hash]
   595                     ctx = repo[hash]
   596                     displayer.show(ctx)
   596                     displayer.show(ctx)
   597                 except error.RepoLookupError as e:
   597                 except error.RepoLookupError as e:
   598                     fm.plain("%s\n\n" % pycompat.bytestr(e))
   598                     fm.plain(b"%s\n\n" % pycompat.bytestr(e))
   599             displayer.close()
   599             displayer.close()
   600 
   600 
   601     fm.end()
   601     fm.end()
   602 
   602 
   603     if entry is None:
   603     if entry is None:
   604         ui.status(_("no recorded locations\n"))
   604         ui.status(_(b"no recorded locations\n"))