branching: merge with stable
authorPierre-Yves David <pierre-yves.david@octobus.net>
Tue, 10 Dec 2019 20:47:13 +0100
changeset 5000 8d38ad9c044f
parent 4986 1214f3d085a9 (diff)
parent 4999 a9457b9aca4e (current diff)
child 5003 ad1ea34335cb
branching: merge with stable
CHANGELOG
hgext3rd/evolve/cmdrewrite.py
hgext3rd/evolve/evolvecmd.py
--- a/.gitlab-ci.yml	Tue Dec 10 20:35:56 2019 +0100
+++ b/.gitlab-ci.yml	Tue Dec 10 20:47:13 2019 +0100
@@ -34,3 +34,14 @@
         - hg_rev=$(tests/testlib/map-hg-rev.sh "$(hg log -r . -T '{branch}')")
         - hg -R /ci/repos/mercurial/ update "$hg_rev"
         - (cd tests; python3 /ci/repos/mercurial/tests/run-tests.py --color=always --pure)
+
+doc:
+    image: octobus/ci-py2-evolve-doc
+    script:
+        - cd docs/
+        - make
+    variables:
+        LANG: en_us.UTF-8
+    artifacts:
+        paths:
+            - html/*
--- a/CHANGELOG	Tue Dec 10 20:35:56 2019 +0100
+++ b/CHANGELOG	Tue Dec 10 20:47:13 2019 +0100
@@ -1,6 +1,15 @@
 Changelog
 =========
 
+9.3.0 - in progress
+-------------------
+
+  * exchange: dropped more bundle-1 related dead code
+  * help: categorizing evolve and topic commands
+  * obslog: make templatable
+  * compat: cleanup some compatibility code for mercurial < 4.5
+  * compat: compatibility with some changes of the upcoming Mercurial 5.3
+
 9.2.2 - in progress
 -------------------
 
--- a/hgext3rd/evolve/__init__.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/__init__.py	Tue Dec 10 20:47:13 2019 +0100
@@ -465,7 +465,8 @@
     _alias, statuscmd = cmdutil.findcmd(b'status', commands.table)
     pstatusopts = [o for o in statuscmd[1] if o[1] != b'rev']
 
-    @eh.command(b'pstatus', pstatusopts)
+    @eh.command(b'pstatus', pstatusopts,
+                **compat.helpcategorykwargs('CATEGORY_WORKING_DIRECTORY'))
     def pstatus(ui, repo, *args, **kwargs):
         """show status combining committed and uncommited changes
 
@@ -480,7 +481,8 @@
     _alias, diffcmd = cmdutil.findcmd(b'diff', commands.table)
     pdiffopts = [o for o in diffcmd[1] if o[1] != b'rev']
 
-    @eh.command(b'pdiff', pdiffopts)
+    @eh.command(b'pdiff', pdiffopts,
+                **compat.helpcategorykwargs('CATEGORY_WORKING_DIRECTORY'))
     def pdiff(ui, repo, *args, **kwargs):
         """show diff combining committed and uncommited changes
 
@@ -989,7 +991,8 @@
      (b'n', b'dry-run', False,
         _(b'do not perform actions, just print what would be done'))],
     b'[OPTION]...',
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_WORKING_DIRECTORY'))
 def cmdprevious(ui, repo, **opts):
     """update to parent revision
 
@@ -1048,7 +1051,8 @@
      (b'n', b'dry-run', False,
       _(b'do not perform actions, just print what would be done'))],
     b'[OPTION]...',
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_WORKING_DIRECTORY'))
 def cmdnext(ui, repo, **opts):
     """update to next child revision
 
@@ -1339,8 +1343,16 @@
         if entry[0] == b"evolution":
             break
     else:
-        help.helptable.append(([b"evolution"], _(b"Safely Rewriting History"),
-                               _helploader))
+        if util.safehasattr(help, 'TOPIC_CATEGORY_CONCEPTS'):
+            help.helptable.append(([b"evolution"],
+                                   _(b"Safely Rewriting History"),
+                                   _helploader,
+                                   help.TOPIC_CATEGORY_CONCEPTS))
+        else:
+            # hg <= 4.7 (c303d65d2e34)
+            help.helptable.append(([b"evolution"],
+                                   _(b"Safely Rewriting History"),
+                                   _helploader))
         help.helptable.sort()
 
 evolvestateversion = 0
--- a/hgext3rd/evolve/cmdrewrite.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/cmdrewrite.py	Tue Dec 10 20:47:13 2019 +0100
@@ -109,7 +109,8 @@
      (b'n', b'note', b'', _(b'store a note on amend'), _(b'TEXT')),
      ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt,
     _(b'[OPTION]... [FILE]...'),
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_COMMITTING'))
 def amend(ui, repo, *pats, **opts):
     """combine a changeset with updates and replace it with a new one
 
@@ -291,8 +292,10 @@
 
 def _touchedbetween(repo, source, dest, match=None):
     touched = set()
-    for files in repo.status(source, dest, match=match)[:3]:
-        touched.update(files)
+    st = repo.status(source, dest, match=match)
+    touched.update(st.modified)
+    touched.update(st.added)
+    touched.update(st.removed)
     return touched
 
 def _commitfiltered(repo, ctx, match, target=None, message=None, user=None,
@@ -364,8 +367,8 @@
         # and considering only the files which are changed between oldctx and
         # ctx, and the status of what changed between oldctx and ctx will help
         # us in defining the exact behavior
-        m, a, r = repo.status(oldctx, ctx, match=match)[:3]
-        for f in m:
+        st = repo.status(oldctx, ctx, match=match)
+        for f in st.modified:
             # These are files which are modified between oldctx and ctx which
             # contains two cases: 1) Were modified in oldctx and some
             # modifications are uncommitted
@@ -381,7 +384,7 @@
                 continue
             ds.normallookup(f)
 
-        for f in a:
+        for f in st.added:
             # These are the files which are added between oldctx and ctx(new
             # one), which means the files which were removed in oldctx
             # but uncommitted completely while making the ctx
@@ -394,7 +397,7 @@
                 continue
             ds.remove(f)
 
-        for f in r:
+        for f in st.removed:
             # These are files which are removed between oldctx and ctx, which
             # means the files which were added in oldctx and were completely
             # uncommitted in ctx. If a added file is partially uncommitted, that
@@ -408,21 +411,21 @@
                 continue
             ds.add(f)
     else:
-        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
-        for f in m:
+        st = repo.status(oldctx.p1(), oldctx, match=match)
+        for f in st.modified:
             if ds[f] == b'r':
                 # modified + removed -> removed
                 continue
             ds.normallookup(f)
 
-        for f in a:
+        for f in st.added:
             if ds[f] == b'r':
                 # added + removed -> unknown
                 ds.drop(f)
             elif ds[f] != b'a':
                 ds.add(f)
 
-        for f in r:
+        for f in st.removed:
             if ds[f] == b'a':
                 # removed + added -> normal
                 ds.normallookup(f)
@@ -434,8 +437,8 @@
     if interactive:
         # Interactive had different meaning of the variables so restoring the
         # original meaning to use them
-        m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
-    for f in (m + a):
+        st = repo.status(oldctx.p1(), oldctx, match=match)
+    for f in (st.modified + st.added):
         src = oldctx[f].renamed()
         if src:
             oldcopies[f] = src[0]
@@ -456,7 +459,8 @@
      (b'', b'revert', False, _(b'discard working directory changes after uncommit')),
      (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
      ] + commands.walkopts + commitopts + commitopts2 + commitopts3,
-    _(b'[OPTION]... [FILE]...'))
+    _(b'[OPTION]... [FILE]...'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def uncommit(ui, repo, *pats, **opts):
     """move changes from parent revision to working directory
 
@@ -692,7 +696,8 @@
      (b'n', b'note', b'', _(b'store a note on fold'), _(b'TEXT')),
      ] + commitopts + commitopts2 + commitopts3,
     _(b'hg fold [OPTION]... [-r] REV...'),
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def fold(ui, repo, *revs, **opts):
     """fold multiple revisions into a single one
 
@@ -822,7 +827,8 @@
      (b'', b'fold', None, _(b"also fold specified revisions into one")),
      (b'n', b'note', b'', _(b'store a note on metaedit'), _(b'TEXT')),
      ] + commitopts + commitopts2 + commitopts3,
-    _(b'hg metaedit [OPTION]... [[-r] REV]...'))
+    _(b'hg metaedit [OPTION]... [[-r] REV]...'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def metaedit(ui, repo, *revs, **opts):
     """edit commit information
 
@@ -980,7 +986,8 @@
      (b'B', b'bookmark', [], _(b"remove revs only reachable from given"
                                b" bookmark"), _(b'BOOKMARK'))] + metadataopts,
     _(b'[OPTION]... [-r] REV...'),
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 # XXX -U  --noupdate option to prevent wc update and or bookmarks update ?
 def cmdprune(ui, repo, *revs, **opts):
     """mark changesets as obsolete or succeeded by another changeset
@@ -1174,7 +1181,8 @@
      (b'n', b'note', b'', _(b"store a note on split"), _(b'TEXT')),
      ] + commitopts + commitopts2 + commitopts3,
     _(b'hg split [OPTION]... [-r REV] [FILE]...'),
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def cmdsplit(ui, repo, *pats, **opts):
     """split a changeset into smaller changesets
 
@@ -1230,8 +1238,8 @@
         rewriteutil.presplitupdate(repo, ui, prev, ctx)
 
         def haschanges(matcher=None):
-            modified, added, removed, deleted = repo.status(match=matcher)[:4]
-            return modified or added or removed or deleted
+            st = repo.status(match=matcher)
+            return st.modified or st.added or st.removed or st.deleted
         msg = (b"HG: This is the original pre-split commit message. "
                b"Edit it as appropriate.\n\n")
         msg += ctx.description()
@@ -1294,10 +1302,12 @@
                         ui.status(_(b'discarding remaining changes\n'))
                         target = newcommits[0]
                         if pats:
-                            status = repo.status(match=matcher)[:4]
+                            status = repo.status(match=matcher)
                             dirty = set()
-                            for i in status:
-                                dirty.update(i)
+                            dirty.update(status.modified)
+                            dirty.update(status.added)
+                            dirty.update(status.removed)
+                            dirty.update(status.deleted)
                             dirty = sorted(dirty)
                             cmdutil.revert(ui, repo, repo[target],
                                            (target, node.nullid), *dirty)
@@ -1349,7 +1359,8 @@
       b'mark the new revision as successor of the old one potentially creating '
       b'divergence')],
     # allow to choose the seed ?
-    _(b'[OPTION]... [-r] REV...'))
+    _(b'[OPTION]... [-r] REV...'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def touch(ui, repo, *revs, **opts):
     """create successors identical to their predecessors but the changeset ID
 
@@ -1450,7 +1461,8 @@
      (b'c', b'continue', False, b'continue interrupted pick'),
      (b'a', b'abort', False, b'abort interrupted pick'),
      ] + mergetoolopts,
-    _(b'[OPTION]... [-r] REV'))
+    _(b'[OPTION]... [-r] REV'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def cmdpick(ui, repo, *revs, **opts):
     """move a commit on the top of working directory parent and updates to it."""
 
--- a/hgext3rd/evolve/compat.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/compat.py	Tue Dec 10 20:47:13 2019 +0100
@@ -17,6 +17,7 @@
     obsolete,
     obsutil,
     pycompat,
+    registrar,
     repair,
     scmutil,
     util,
@@ -523,6 +524,15 @@
 
     return sorted(operations)
 
+# help category compatibility
+# hg <= 4.7 (c303d65d2e34)
+def helpcategorykwargs(categoryname):
+    """Backwards-compatible specification of the helpategory argument."""
+    category = getattr(registrar.command, categoryname, None)
+    if not category:
+        return {}
+    return {'helpcategory': category}
+
 # nodemap.get and index.[has_node|rev|get_rev]
 # hg <= 5.3 (02802fa87b74)
 def getgetrev(cl):
--- a/hgext3rd/evolve/evolvecmd.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/evolvecmd.py	Tue Dec 10 20:47:13 2019 +0100
@@ -1539,7 +1539,8 @@
                               b' in the repo')),
      ] + mergetoolopts,
     _(b'[OPTIONS]...'),
-    helpbasic=True
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT')
 )
 def evolve(ui, repo, **opts):
     """solve troubled changesets in your repository
--- a/hgext3rd/evolve/exthelper.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/exthelper.py	Tue Dec 10 20:47:13 2019 +0100
@@ -50,7 +50,7 @@
         @eh.command('mynewcommand',
             [('r', 'rev', [], _('operate on these revisions'))],
             _('-r REV...'),
-            helpcategory=command.CATEGORY_XXX)
+            **compat.helpcategorykwargs('CATEGORY_XXX'))
         def newcommand(ui, repo, *revs, **opts):
             # implementation goes here
 
--- a/hgext3rd/evolve/genericcaches.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/genericcaches.py	Tue Dec 10 20:47:13 2019 +0100
@@ -14,7 +14,7 @@
     util,
 )
 
-class incrementalcachebase(object):
+class incrementalcachebase(object):  # pytype: disable=ignored-metaclass
     """base class for incremental cache from append only source
 
     There are multiple append only data source we might want to cache
@@ -133,7 +133,7 @@
         """read the cachekey from bytes"""
         return self._cachekeystruct.unpack(data)
 
-class changelogsourcebase(incrementalcachebase):
+class changelogsourcebase(incrementalcachebase):  # pytype: disable=ignored-metaclass
     """an abstract class for cache sourcing data from the changelog
 
     For this purpose it use a cache key covering changelog content.
--- a/hgext3rd/evolve/metadata.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/metadata.py	Tue Dec 10 20:47:13 2019 +0100
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-__version__ = b'9.2.2.dev'
+__version__ = b'9.3.0.dev'
 testedwith = b'4.5.2 4.6.2 4.7 4.8 4.9 5.0 5.1'
 minimumhgversion = b'4.5'
 buglink = b'https://bz.mercurial-scm.org/'
--- a/hgext3rd/evolve/obsexchange.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/obsexchange.py	Tue Dec 10 20:47:13 2019 +0100
@@ -7,17 +7,11 @@
 
 from __future__ import absolute_import
 
-try:
-    from StringIO import StringIO
-except ImportError:
-    from io import StringIO
-
 from mercurial import (
     bundle2,
     error,
     exchange,
     extensions,
-    lock as lockmod,
     node,
     obsolete,
     pushkey,
@@ -35,7 +29,6 @@
 eh = exthelper.exthelper()
 eh.merge(obsdiscovery.eh)
 obsexcmsg = utility.obsexcmsg
-obsexcprg = utility.obsexcprg
 
 eh.configitem(b'experimental', b'verbose-obsolescence-exchange', False)
 
@@ -156,82 +149,6 @@
         return _obscommon_capabilities(oldcap, repo, proto)
     wireprotov1server.commands[b'capabilities'] = (newcap, args)
 
-def _pushobsmarkers(repo, data):
-    tr = lock = None
-    try:
-        lock = repo.lock()
-        tr = repo.transaction(b'pushkey: obsolete markers')
-        new = repo.obsstore.mergemarkers(tr, data)
-        if new is not None:
-            obsexcmsg(repo.ui, b"%i obsolescence markers added\n" % new, True)
-        tr.close()
-    finally:
-        lockmod.release(tr, lock)
-    repo.hook(b'evolve_pushobsmarkers')
-
-def srv_pushobsmarkers(repo, proto):
-    """wireprotocol command"""
-    fp = StringIO()
-    proto.redirect()
-    proto.getfile(fp)
-    data = fp.getvalue()
-    fp.close()
-    _pushobsmarkers(repo, data)
-    try:
-        from mercurial import wireprototypes
-        wireprototypes.pushres # force demandimport
-    except (ImportError, AttributeError):
-        from mercurial import wireproto as wireprototypes
-    return wireprototypes.pushres(0)
-
-def _getobsmarkersstream(repo, heads=None, common=None):
-    """Get a binary stream for all markers relevant to `::<heads> - ::<common>`
-    """
-    revset = b''
-    args = []
-    repo = repo.unfiltered()
-    if heads is None:
-        revset = b'all()'
-    elif heads:
-        revset += b"(::%ln)"
-        args.append(heads)
-    else:
-        assert False, b'pulling no heads?'
-    if common:
-        revset += b' - (::%ln)'
-        args.append(common)
-    nodes = [c.node() for c in repo.set(revset, *args)]
-    markers = repo.obsstore.relevantmarkers(nodes)
-    obsdata = StringIO()
-    for chunk in obsolete.encodemarkers(markers, True):
-        obsdata.write(chunk)
-    obsdata.seek(0)
-    return obsdata
-
-def srv_pullobsmarkers(repo, proto, others):
-    """serves a binary stream of markers.
-
-    Serves relevant to changeset between heads and common. The stream is prefix
-    by a -string- representation of an integer. This integer is the size of the
-    stream."""
-    try:
-        from mercurial import wireprototypes, wireprotov1server
-        wireprototypes.pushres # force demandimport
-    except (ImportError, AttributeError):
-        from mercurial import wireproto as wireprototypes
-        wireprotov1server = wireprototypes
-    opts = wireprotov1server.options(b'', [b'heads', b'common'], others)
-    for k, v in opts.items():
-        if k in (b'heads', b'common'):
-            opts[k] = wireprototypes.decodelist(v)
-    obsdata = _getobsmarkersstream(repo, **opts)
-    finaldata = StringIO()
-    obsdata = obsdata.getvalue()
-    finaldata.write(b'%20i' % len(obsdata))
-    finaldata.write(obsdata)
-    finaldata.seek(0)
-    return wireprototypes.streamres(reader=finaldata, v1compressible=True)
-
 abortmsg = b"won't exchange obsmarkers through pushkey"
 hint = b"upgrade your client or server to use the bundle2 protocol"
 
--- a/hgext3rd/evolve/obshistory.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/obshistory.py	Tue Dec 10 20:47:13 2019 +0100
@@ -50,7 +50,8 @@
      (b'p', b'patch', False, _(b'show the patch between two obs versions')),
      (b'f', b'filternonlocal', False, _(b'filter out non local commits')),
      ] + commands.formatteropts,
-    _(b'hg olog [OPTION]... [[-r] REV]...'))
+    _(b'hg olog [OPTION]... [[-r] REV]...'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION'))
 def cmdobshistory(ui, repo, *revs, **opts):
     """show the obsolescence history of the specified revisions
 
@@ -85,6 +86,13 @@
         revs = [b'.']
     revs = scmutil.revrange(repo, revs)
 
+    # Use the default template unless the user provided one, but not if
+    # -f was given, because that doesn't work with templates yet. Note
+    # that --no-graph doesn't support -f (it ignores it), so we also
+    # don't use templating with --no-graph.
+    if not opts['template'] and not (opts['filternonlocal'] and opts['graph']):
+        opts['template'] = DEFAULT_TEMPLATE
+
     if opts['graph']:
         return _debugobshistorygraph(ui, repo, revs, opts)
 
@@ -135,6 +143,35 @@
 
     return values
 
+TEMPLATE_MISSING_NODE = b"""{label("evolve.node evolve.missing_change_ctx", node|short)}"""
+TEMPLATE_PRESENT_NODE = b"""{label("evolve.node", node|short)} {label("evolve.rev", "({rev})")} {label("evolve.short_description", desc|firstline)}"""
+TEMPLATE_FIRST_LINE = b"""{if(rev, "%(presentnode)s", "%(missingnode)s")}""" % {
+    b"presentnode": TEMPLATE_PRESENT_NODE,
+    b"missingnode": TEMPLATE_MISSING_NODE
+}
+TEMPLATE_VERB = b"""{label("evolve.verb", verb)}"""
+TEMPLATE_SUCCNODES = b"""{label("evolve.node", join(succnodes % "{succnode|short}", ", "))}"""
+TEMPLATE_REWRITE = b"""{if(succnodes, "%(verb)s{if(effects, "({join(effects, ", ")})")} as %(succnodes)s", "pruned")}""" % {
+    b"verb": TEMPLATE_VERB,
+    b"succnodes": TEMPLATE_SUCCNODES
+}
+TEMPLATE_OPERATION = b"""{if(operation, "using {label("evolve.operation", operation)}")}"""
+TEMPLATE_USER = b"""by {label("evolve.user", user)}"""
+TEMPLATE_DATE = b"""{label("evolve.date", "({date(date, "%a %b %d %H:%M:%S %Y %1%2")})")}"""
+TEMPLATE_NOTE = b"""{if(note, "\n    note: {label("evolve.note", note)}")}"""
+TEMPLATE_PATCH = b"""{if(patch, "{patch}")}{if(nopatchreason, "\n(No patch available, {nopatchreason})")}"""
+DEFAULT_TEMPLATE = (b"""%(firstline)s
+{markers %% "  {separate(" ", "%(rewrite)s", "%(operation)s", "%(user)s", "%(date)s")}%(note)s{indent(descdiff, "    ")}{indent("%(patch)s", "    ")}\n"}
+""") % {
+    b"firstline": TEMPLATE_FIRST_LINE,
+    b"rewrite": TEMPLATE_REWRITE,
+    b"operation": TEMPLATE_OPERATION,
+    b"user": TEMPLATE_USER,
+    b"date": TEMPLATE_DATE,
+    b"note": TEMPLATE_NOTE,
+    b"patch": TEMPLATE_PATCH,
+}
+
 class obsmarker_printer(compat.changesetprinter):
     """show (available) information about a node
 
@@ -178,7 +215,7 @@
                 succs = sorted(succs)
 
                 for successor in succs:
-                    _debugobshistorydisplaymarker(markerfm, successor,
+                    _debugobshistorydisplaymarker(self.ui, markerfm, successor,
                                                   ctx.node(), self.repo,
                                                   self._includediff)
 
@@ -190,11 +227,11 @@
                     if not markers:
                         continue
                     successors = succset[b"successors"]
-                    _debugobshistorydisplaysuccsandmarkers(markerfm, successors, markers, ctx.node(), self.repo, self._includediff)
+                    _debugobshistorydisplaysuccsandmarkers(self.ui, markerfm, successors, markers, ctx.node(), self.repo, self._includediff)
 
             markerfm.end()
 
-            markerfm.plain(b'\n')
+            fm.plain(b'\n')
             fm.end()
 
             self.hunk[ctx.node()] = self.ui.popbuffer()
@@ -454,8 +491,9 @@
         markerfm = fm.nested(b"markers")
         for successor in sorted(succs):
             includediff = opts and opts.get("patch")
-            _debugobshistorydisplaymarker(markerfm, successor, ctxnode, unfi, includediff)
+            _debugobshistorydisplaymarker(ui, markerfm, successor, ctxnode, unfi, includediff)
         markerfm.end()
+        fm.plain(b'\n')
 
         precs = precursors.get(ctxnode, ())
         for p in sorted(precs):
@@ -477,12 +515,12 @@
         shortdescription = shortdescription.splitlines()[0]
 
     fm.startitem()
-    fm.write(b'node', b'%s', bytes(ctx),
-             label=b"evolve.node")
+    fm.context(ctx=ctx)
+    fm.data(node=ctx.hex())
+    fm.plain(b'%s' % bytes(ctx), label=b"evolve.node")
     fm.plain(b' ')
 
-    fm.write(b'rev', b'(%d)', ctx.rev(),
-             label=b"evolve.rev")
+    fm.plain(b'(%d)' % ctx.rev(), label=b"evolve.rev")
     fm.plain(b' ')
 
     fm.write(b'shortdescription', b'%s', shortdescription,
@@ -490,19 +528,18 @@
     fm.plain(b'\n')
 
 def _debugobshistorydisplaymissingctx(fm, nodewithoutctx):
-    hexnode = nodemod.short(nodewithoutctx)
     fm.startitem()
-    fm.write(b'node', b'%s', hexnode,
+    fm.data(node=nodemod.hex(nodewithoutctx))
+    fm.plain(nodemod.short(nodewithoutctx),
              label=b"evolve.node evolve.missing_change_ctx")
     fm.plain(b'\n')
 
-def _debugobshistorydisplaymarker(fm, marker, node, repo, includediff=False):
+def _debugobshistorydisplaymarker(ui, fm, marker, node, repo, includediff=False):
     succnodes = marker[1]
     date = marker[4]
     metadata = dict(marker[3])
 
     fm.startitem()
-    fm.plain(b'  ')
 
     # Detect pruned revisions
     if len(succnodes) == 0:
@@ -510,8 +547,7 @@
     else:
         verb = b'rewritten'
 
-    fm.write(b'verb', b'%s', verb,
-             label=b"evolve.verb")
+    fm.data(verb=verb)
 
     effectflag = metadata.get(b'ef1')
     if effectflag is not None:
@@ -520,54 +556,44 @@
         except ValueError:
             effectflag = None
     if effectflag:
-        effect = []
+        effects = []
 
         # XXX should be a dict
         if effectflag & DESCCHANGED:
-            effect.append(b'description')
+            effects.append(b'description')
         if effectflag & METACHANGED:
-            effect.append(b'meta')
+            effects.append(b'meta')
         if effectflag & USERCHANGED:
-            effect.append(b'user')
+            effects.append(b'user')
         if effectflag & DATECHANGED:
-            effect.append(b'date')
+            effects.append(b'date')
         if effectflag & BRANCHCHANGED:
-            effect.append(b'branch')
+            effects.append(b'branch')
         if effectflag & PARENTCHANGED:
-            effect.append(b'parent')
+            effects.append(b'parent')
         if effectflag & DIFFCHANGED:
-            effect.append(b'content')
+            effects.append(b'content')
 
-        if effect:
-            fmteffect = fm.formatlist(effect, b'effect', sep=b', ')
-            fm.write(b'effect', b'(%s)', fmteffect)
+        if effects:
+            fmteffect = fm.formatlist(effects, b'effect')
+            fm.write(b'effects', b'(%s)', fmteffect)
 
     if len(succnodes) > 0:
-        fm.plain(b' as ')
-
-        shortsnodes = (nodemod.short(succnode) for succnode in sorted(succnodes))
-        nodes = fm.formatlist(shortsnodes, b'succnodes', sep=b', ')
-        fm.write(b'succnodes', b'%s', nodes,
-                 label=b"evolve.node")
+        hexnodes = (nodemod.hex(succnode) for succnode in sorted(succnodes))
+        nodes = fm.formatlist(hexnodes, b'succnode')
+        fm.write(b'succnodes', b'%s', nodes)
 
     operation = metadata.get(b'operation')
     if operation:
-        fm.plain(b' using ')
-        fm.write(b'operation', b'%s', operation, label=b"evolve.operation")
-
-    fm.plain(b' by ')
+        fm.data(operation=operation)
 
-    fm.write(b'user', b'%s', metadata[b'user'],
-             label=b"evolve.user")
-    fm.plain(b' ')
+    fm.data(user=metadata[b'user'])
 
-    fm.write(b'date', b'(%s)', fm.formatdate(date),
-             label=b"evolve.date")
+    fm.data(date=date)
 
     # initial support for showing note
     if metadata.get(b'note'):
-        fm.plain(b'\n    note: ')
-        fm.write(b'note', b"%s", metadata[b'note'], label=b"evolve.note")
+        fm.data(note=metadata[b'note'])
 
     # Patch display
     if includediff is True:
@@ -592,15 +618,16 @@
                 def tolist(text):
                     return [text]
 
-                fm.plain(b"\n")
+                ui.pushbuffer(labeled=True)
+                ui.write(b"\n")
 
                 for chunk, label in patch.difflabel(tolist, descriptionpatch):
                     chunk = chunk.strip(b'\t')
-                    if chunk and chunk != b'\n':
-                        fm.plain(b'    ')
-                    fm.write(b'desc-diff', b'%s', chunk, label=label)
+                    ui.write(chunk, label=label)
+                fm.write(b'descdiff', b'%s', ui.popbuffer())
 
             # Content patch
+            ui.pushbuffer(labeled=True)
             diffopts = patch.diffallopts(repo.ui, {})
             matchfn = scmutil.matchall(repo)
             firstline = True
@@ -608,23 +635,18 @@
             for chunk, label in patch.diffui(repo, node, succ, matchfn,
                                              opts=diffopts):
                 if firstline:
-                    fm.plain(b'\n')
+                    ui.write(b'\n')
                     firstline = False
                 if linestart:
-                    fm.plain(b'    ')
                     linestart = False
                 if chunk == b'\n':
                     linestart = True
-                fm.write(b'patch', b'%s', chunk, label=label)
+                ui.write(chunk, label=label)
+            fm.data(patch=ui.popbuffer())
         else:
-            nopatch = b"    (No patch available, %s)" % _patchavailable[1]
-            fm.plain(b"\n")
-            # TODO: should be in json too
-            fm.plain(nopatch)
+            fm.data(nopatchreason=_patchavailable[1])
 
-    fm.plain(b"\n")
-
-def _debugobshistorydisplaysuccsandmarkers(fm, succnodes, markers, node, repo, includediff=False):
+def _debugobshistorydisplaysuccsandmarkers(ui, fm, succnodes, markers, node, repo, includediff=False):
     """
     This function is a duplication of _debugobshistorydisplaymarker modified
     to accept multiple markers as input.
@@ -648,33 +670,33 @@
             effectflag |= int(ef)
 
     if effectflag:
-        effect = []
+        effects = []
 
         # XXX should be a dict
         if effectflag & DESCCHANGED:
-            effect.append(b'description')
+            effects.append(b'description')
         if effectflag & METACHANGED:
-            effect.append(b'meta')
+            effects.append(b'meta')
         if effectflag & USERCHANGED:
-            effect.append(b'user')
+            effects.append(b'user')
         if effectflag & DATECHANGED:
-            effect.append(b'date')
+            effects.append(b'date')
         if effectflag & BRANCHCHANGED:
-            effect.append(b'branch')
+            effects.append(b'branch')
         if effectflag & PARENTCHANGED:
-            effect.append(b'parent')
+            effects.append(b'parent')
         if effectflag & DIFFCHANGED:
-            effect.append(b'content')
+            effects.append(b'content')
 
-        if effect:
-            fmteffect = fm.formatlist(effect, b'effect', sep=b', ')
-            fm.write(b'effect', b'(%s)', fmteffect)
+        if effects:
+            fmteffect = fm.formatlist(effects, b'effect', sep=b', ')
+            fm.write(b'effects', b'(%s)', fmteffect)
 
     if len(succnodes) > 0:
         fm.plain(b' as ')
 
         shortsnodes = (nodemod.short(succnode) for succnode in sorted(succnodes))
-        nodes = fm.formatlist(shortsnodes, b'succnodes', sep=b', ')
+        nodes = fm.formatlist(shortsnodes, b'succnode', sep=b', ')
         fm.write(b'succnodes', b'%s', nodes,
                  label=b"evolve.node")
 
@@ -732,15 +754,18 @@
                 def tolist(text):
                     return [text]
 
-                fm.plain(b"\n")
+                ui.pushbuffer(labeled=True)
+                ui.write(b"\n")
 
                 for chunk, label in patch.difflabel(tolist, descriptionpatch):
                     chunk = chunk.strip(b'\t')
                     if chunk and chunk != b'\n':
-                        fm.plain(b'    ')
-                    fm.write(b'desc-diff', b'%s', chunk, label=label)
+                        ui.write(b'    ')
+                    ui.write(chunk, label=label)
+                fm.write(b'descdiff', b'%s', ui.popbuffer())
 
             # Content patch
+            ui.pushbuffer(labeled=True)
             diffopts = patch.diffallopts(repo.ui, {})
             matchfn = scmutil.matchall(repo)
             firstline = True
@@ -748,19 +773,18 @@
             for chunk, label in patch.diffui(repo, node, succ, matchfn,
                                              opts=diffopts):
                 if firstline:
-                    fm.plain(b'\n')
+                    ui.write(b'\n')
                     firstline = False
                 if linestart:
-                    fm.plain(b'    ')
+                    ui.write(b'    ')
                     linestart = False
                 if chunk == b'\n':
                     linestart = True
-                fm.write(b'patch', b'%s', chunk, label=label)
+                ui.write(chunk, label=label)
+            fm.write(b'patch', b'%s', ui.popbuffer())
         else:
-            nopatch = b"    (No patch available, %s)" % _patchavailable[1]
-            fm.plain(b"\n")
-            # TODO: should be in json too
-            fm.plain(nopatch)
+            fm.write(b'nopatchreason', b"\n    (No patch available, %s)",
+                     _patchavailable[1])
 
     fm.plain(b"\n")
 
--- a/hgext3rd/evolve/rewind.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/rewind.py	Tue Dec 10 20:47:13 2019 +0100
@@ -36,7 +36,8 @@
       _(b"do not modify working directory during rewind")),
      ],
     _(b'[--as-divergence] [--exact] [--keep] [--to REV]... [--from REV]...'),
-    helpbasic=True)
+    helpbasic=True,
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_MANAGEMENT'))
 def rewind(ui, repo, **opts):
     """rewind a stack of changesets to a previous state
 
--- a/hgext3rd/evolve/utility.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/evolve/utility.py	Tue Dec 10 20:47:13 2019 +0100
@@ -13,10 +13,6 @@
 
 from mercurial.node import nullrev
 
-from . import (
-    compat,
-)
-
 shorttemplate = b"[{label('evolve.rev', rev)}] {desc|firstline}\n"
 stacktemplate = b"""[{label('evolve.rev', if(topicidx, "s{topicidx}", rev))}] {desc|firstline}\n"""
 
@@ -27,12 +23,6 @@
     if important or verbose:
         ui.status(message)
 
-def obsexcprg(ui, *args, **kwargs):
-    topic = b'obsmarkers exchange'
-    if ui.configbool(b'experimental', b'verbose-obsolescence-exchange'):
-        topic = b'OBSEXC'
-    compat.progress(ui, topic, *args, **kwargs)
-
 def filterparents(parents):
     """filter nullrev parents
 
--- a/hgext3rd/topic/__init__.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/topic/__init__.py	Tue Dec 10 20:47:13 2019 +0100
@@ -135,6 +135,7 @@
     namespaces,
     node,
     obsolete,
+    obsutil,
     patch,
     phases,
     pycompat,
@@ -187,7 +188,7 @@
               b'topic.active': b'green',
               }
 
-__version__ = b'0.17.2.dev'
+__version__ = b'0.18.0.dev'
 
 testedwith = b'4.5.2 4.6.2 4.7 4.8 4.9 5.0 5.1'
 minimumhgversion = b'4.5'
@@ -634,7 +635,8 @@
         (b'', b'age', False, b'show when you last touched the topics'),
         (b'', b'current', None, b'display the current topic only'),
     ] + commands.formatteropts,
-    _(b'hg topics [OPTION]... [-r REV]... [TOPIC]'))
+    _(b'hg topics [OPTION]... [-r REV]... [TOPIC]'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_ORGANIZATION'))
 def topics(ui, repo, topic=None, **opts):
     """View current topic, set current topic, change topic for a set of revisions, or see all topics.
 
@@ -777,7 +779,8 @@
         (b'c', b'children', None,
             _(b'display data about children outside of the stack'))
     ] + commands.formatteropts,
-    _(b'hg stack [TOPIC]'))
+    _(b'hg stack [TOPIC]'),
+    **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION'))
 def cmdstack(ui, repo, topic=b'', **opts):
     """list all changesets in a topic and other information
 
@@ -1115,7 +1118,7 @@
                 user = repo[revs].user()
             # looking on the markers also to get more information and accurate
             # last touch time.
-            obsmarkers = compat.getmarkers(repo, [repo[revs].node()])
+            obsmarkers = obsutil.getmarkers(repo, [repo[revs].node()])
             for marker in obsmarkers:
                 rt = marker.date()
                 if rt[0] > maxtime[0]:
--- a/hgext3rd/topic/compat.py	Tue Dec 10 20:35:56 2019 +0100
+++ b/hgext3rd/topic/compat.py	Tue Dec 10 20:47:13 2019 +0100
@@ -8,25 +8,11 @@
 from __future__ import absolute_import
 
 from mercurial import (
-    obsolete,
     pycompat,
+    registrar,
     util,
 )
 
-getmarkers = None
-successorssets = None
-try:
-    from mercurial import obsutil
-    getmarkers = getattr(obsutil, 'getmarkers', None)
-    successorssets = getattr(obsutil, 'successorssets', None)
-except ImportError:
-    pass
-
-if getmarkers is None:
-    getmarkers = obsolete.getmarkers
-if successorssets is None:
-    successorssets = obsolete.successorssets
-
 if pycompat.ispy3:
     def branchmapitems(branchmap):
         return branchmap.items()
@@ -36,6 +22,15 @@
         return branchmap.iteritems()
     # py3-transform: on
 
+# help category compatibility
+# hg <= 4.7 (c303d65d2e34)
+def helpcategorykwargs(categoryname):
+    """Backwards-compatible specification of the helpategory argument."""
+    category = getattr(registrar.command, categoryname, None)
+    if not category:
+        return {}
+    return {'helpcategory': category}
+
 # nodemap.get and index.[has_node|rev|get_rev]
 # hg <= 5.3 (02802fa87b74)
 def getgetrev(cl):
--- a/tests/test-evolve-cycles.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-cycles.t	Tue Dec 10 20:47:13 2019 +0100
@@ -300,21 +300,20 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "0da815c333f6"
+                      "0da815c333f6364b46c86b0a897c00eb617397b6"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "868d2e0eb19c",
-          "rev": 4,
+          "node": "868d2e0eb19c2b55a2894d37e1c435c221384d48",
           "shortdescription": "D"
       },
       {
@@ -324,21 +323,20 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "868d2e0eb19c"
+                      "868d2e0eb19c2b55a2894d37e1c435c221384d48"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "d9f908fde1a1",
-          "rev": 6,
+          "node": "d9f908fde1a10ad198a462a3ec8b440bb397fc9c",
           "shortdescription": "F"
       },
       {
@@ -348,21 +346,20 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "d9f908fde1a1"
+                      "d9f908fde1a10ad198a462a3ec8b440bb397fc9c"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "0da815c333f6",
-          "rev": 5,
+          "node": "0da815c333f6364b46c86b0a897c00eb617397b6",
           "shortdescription": "E"
       },
       {
@@ -372,22 +369,21 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "2a34000d3544",
-                      "868d2e0eb19c"
+                      "2a34000d35446022104f7a091c06fe21ff2b5912",
+                      "868d2e0eb19c2b55a2894d37e1c435c221384d48"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "a8df460dbbfe",
-          "rev": 3,
+          "node": "a8df460dbbfe9ef0c1e5ab4fff02e9514672e379",
           "shortdescription": "C"
       },
       {
@@ -397,21 +393,20 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "a8df460dbbfe"
+                      "a8df460dbbfe9ef0c1e5ab4fff02e9514672e379"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "c473644ee0e9",
-          "rev": 2,
+          "node": "c473644ee0e988d7f537e31423831bbc409f12f7",
           "shortdescription": "B"
       },
       {
@@ -421,21 +416,20 @@
                       *, (glob)
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "prune",
                   "succnodes": [
-                      "c473644ee0e9"
+                      "c473644ee0e988d7f537e31423831bbc409f12f7"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "2a34000d3544",
-          "rev": 1,
+          "node": "2a34000d35446022104f7a091c06fe21ff2b5912",
           "shortdescription": "A"
       }
   ]
--- a/tests/test-evolve-obshistory-amend-then-fold.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-amend-then-fold.t	Tue Dec 10 20:47:13 2019 +0100
@@ -152,8 +152,7 @@
   [
       {
           "markers": [],
-          "node": "eb5a0daa2192",
-          "rev": 4,
+          "node": "eb5a0daa21923bbf8caeb2c42085b9e463861fd0",
           "shortdescription": "C0"
       },
       {
@@ -163,21 +162,20 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       *, (glob)
                       *, (glob)
                       "content"
                   ],
                   "operation": "fold",
                   "succnodes": [
-                      "eb5a0daa2192"
+                      "eb5a0daa21923bbf8caeb2c42085b9e463861fd0"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "b7ea6d14e664",
-          "rev": 3,
+          "node": "b7ea6d14e664bdc8922221f7992631b50da3fb07",
           "shortdescription": "B1"
       },
       {
@@ -187,19 +185,18 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "b7ea6d14e664"
+                      "b7ea6d14e664bdc8922221f7992631b50da3fb07"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "0dec01379d3b",
-          "rev": 2,
+          "node": "0dec01379d3be6318c470ead31b1fe7ae7cb53d5",
           "shortdescription": "B0"
       },
       {
@@ -209,20 +206,19 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "content"
                   ],
                   "operation": "fold",
                   "succnodes": [
-                      "eb5a0daa2192"
+                      "eb5a0daa21923bbf8caeb2c42085b9e463861fd0"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
--- a/tests/test-evolve-obshistory-amend.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-amend.t	Tue Dec 10 20:47:13 2019 +0100
@@ -92,6 +92,7 @@
 
   $ hg obslog --no-graph --patch 4ae3a4151de9
   4ae3a4151de9 (2) A1
+  
   471f378eab4c (1) A0
     rewritten(description, content) as 4ae3a4151de9 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       diff -r 471f378eab4c -r 4ae3a4151de9 changeset-description
@@ -109,8 +110,32 @@
       @@ -1,1 +1,2 @@
        A0
       +42
-      
+  
+  
 
+Test that content diff works with templating
+  $ hg obslog --color=debug --patch 4ae3a4151de9 \
+  > -T '{node|short} {desc|firstline}\n{markers % "patch:\n```{patch}```\n"}'
+  @  4ae3a4151de9 A1
+  |
+  x  471f378eab4c A0
+     patch:
+     ```
+     [diff.diffline|diff -r 471f378eab4c -r 4ae3a4151de9 A0]
+     [diff.file_a|--- a/A0	Thu Jan 01 00:00:00 1970 +0000]
+     [diff.file_b|+++ b/A0	Thu Jan 01 00:00:00 1970 +0000]
+     [diff.hunk|@@ -1,1 +1,2 @@]
+      A0
+     [diff.inserted|+42]
+     ```
+
+  $ hg obslog 4ae3a4151de9 --graph -T'{label("log.summary", desc|firstline)} {if(markers, join(markers % "at {date|hgdate} by {user|person} ", " also "))}'
+  @  A1
+  |
+  x  A0 at 0 0 by test
+  
+
+Check that the same thing works with the old {shortdescription} form
   $ hg obslog 4ae3a4151de9 --graph -T'{label("log.summary", shortdescription)} {if(markers, join(markers % "at {date|hgdate} by {user|person} ", " also "))}'
   @  A1
   |
@@ -120,8 +145,7 @@
   [
       {
           "markers": [],
-          "node": "4ae3a4151de9",
-          "rev": 2,
+          "node": "4ae3a4151de9aa872113f0b196e28323308981e8",
           "shortdescription": "A1"
       },
       {
@@ -131,20 +155,19 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "content"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "4ae3a4151de9"
+                      "4ae3a4151de9aa872113f0b196e28323308981e8"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
@@ -177,20 +200,19 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       *, (glob)
                       "content"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "4ae3a4151de9"
+                      "4ae3a4151de9aa872113f0b196e28323308981e8"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
@@ -220,15 +242,19 @@
   
   $ hg obslog -R $TESTTMP/server --no-graph --patch 4ae3a4151de9
   4ae3a4151de9 (1) A1
+  
   471f378eab4c
     rewritten(description, content) as 4ae3a4151de9 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
 
   $ hg obslog -R $TESTTMP/server --no-graph -f --patch 4ae3a4151de9
   4ae3a4151de9 (1) A1
+  
   471f378eab4c
     rewritten(description, content) as 4ae3a4151de9 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
 
 Amend two more times
 ====================
@@ -347,6 +373,50 @@
          +42
   
   
+Test that description diff works with templating
+  $ hg obslog --color=debug --patch 92210308515b \
+  > -T '{node|short} {desc|firstline}\n{markers % "description diff:\n```{descdiff}```\n"}'
+  @  92210308515b A3
+  |
+  x  4f1685185907 A2
+  |  description diff:
+  |  ```
+  |  [diff.diffline|diff -r 4f1685185907 -r 92210308515b changeset-description]
+  |  [diff.file_a|--- a/changeset-description]
+  |  [diff.file_b|+++ b/changeset-description]
+  |  [diff.hunk|@@ -1,3 +1,3 @@]
+  |  [diff.deleted|-A2]
+  |  [diff.inserted|+A3]
+  |
+  |  [diff.deleted|-Better better commit message]
+  |  [diff.inserted|+Better better better commit message]
+  |  ```
+  x  4ae3a4151de9 A1
+  |  description diff:
+  |  ```
+  |  [diff.diffline|diff -r 4ae3a4151de9 -r 4f1685185907 changeset-description]
+  |  [diff.file_a|--- a/changeset-description]
+  |  [diff.file_b|+++ b/changeset-description]
+  |  [diff.hunk|@@ -1,3 +1,3 @@]
+  |  [diff.deleted|-A1]
+  |  [diff.inserted|+A2]
+  |
+  |  [diff.deleted|-Better commit message]
+  |  [diff.inserted|+Better better commit message]
+  |  ```
+  x  471f378eab4c A0
+     description diff:
+     ```
+     [diff.diffline|diff -r 471f378eab4c -r 4ae3a4151de9 changeset-description]
+     [diff.file_a|--- a/changeset-description]
+     [diff.file_b|+++ b/changeset-description]
+     [diff.hunk|@@ -1,1 +1,3 @@]
+     [diff.deleted|-A0]
+     [diff.inserted|+A1]
+     [diff.inserted|+]
+     [diff.inserted|+Better commit message]
+     ```
+
 Check the output on the server
 ------------------------------
 
@@ -383,24 +453,32 @@
   
   $ hg obslog -R $TESTTMP/server --no-graph --patch 92210308515b
   92210308515b (2) A3
+  
   4f1685185907
     rewritten(description) as 92210308515b using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
   4ae3a4151de9 (1) A1
     rewritten(description) as 4f1685185907 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, successor is unknown locally)
+  
   471f378eab4c
     rewritten(description, content) as 4ae3a4151de9 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
 
   $ hg obslog -R $TESTTMP/server --no-graph -f --patch 92210308515b
   92210308515b (2) A3
+  
   4f1685185907
     rewritten(description) as 92210308515b using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
   4ae3a4151de9 (1) A1
     rewritten(description) as 4f1685185907 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, successor is unknown locally)
+  
   471f378eab4c
     rewritten(description, content) as 4ae3a4151de9 using amend by test (Thu Jan 01 00:00:00 1970 +0000)
       (No patch available, context is not local)
+  
--- a/tests/test-evolve-obshistory-content-divergent.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-content-divergent.t	Tue Dec 10 20:47:13 2019 +0100
@@ -129,12 +129,12 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "65b757b745b9"
+                      "65b757b745b935093c87a2bccd877521cccffcbd"
                   ],
                   "user": "test",
                   "verb": "rewritten"
@@ -144,19 +144,18 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "fdf9bde5129a"
+                      "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
@@ -284,8 +283,7 @@
   [
       {
           "markers": [],
-          "node": "65b757b745b9",
-          "rev": 3,
+          "node": "65b757b745b935093c87a2bccd877521cccffcbd",
           "shortdescription": "A2"
       },
       {
@@ -295,12 +293,12 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "65b757b745b9"
+                      "65b757b745b935093c87a2bccd877521cccffcbd"
                   ],
                   "user": "test",
                   "verb": "rewritten"
@@ -310,25 +308,23 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "fdf9bde5129a"
+                      "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       },
       {
           "markers": [],
-          "node": "fdf9bde5129a",
-          "rev": 2,
+          "node": "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e",
           "shortdescription": "A1"
       }
   ]
--- a/tests/test-evolve-obshistory-fold.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-fold.t	Tue Dec 10 20:47:13 2019 +0100
@@ -172,8 +172,7 @@
   [
       {
           "markers": [],
-          "node": "eb5a0daa2192",
-          "rev": 3,
+          "node": "eb5a0daa21923bbf8caeb2c42085b9e463861fd0",
           "shortdescription": "C0"
       },
       {
@@ -183,20 +182,19 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "content"
                   ],
                   "operation": "fold",
                   "succnodes": [
-                      "eb5a0daa2192"
+                      "eb5a0daa21923bbf8caeb2c42085b9e463861fd0"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       },
       {
@@ -206,21 +204,20 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description",
                       "parent",
                       "content"
                   ],
                   "operation": "fold",
                   "succnodes": [
-                      "eb5a0daa2192"
+                      "eb5a0daa21923bbf8caeb2c42085b9e463861fd0"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "0dec01379d3b",
-          "rev": 2,
+          "node": "0dec01379d3be6318c470ead31b1fe7ae7cb53d5",
           "shortdescription": "B0"
       }
   ]
--- a/tests/test-evolve-obshistory-lots-of-splits.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-lots-of-splits.t	Tue Dec 10 20:47:13 2019 +0100
@@ -201,23 +201,22 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "parent",
                       "content"
                   ],
                   "operation": "split",
                   "succnodes": [
-                      "1ae8bc733a14",
-                      "337fec4d2edc",
-                      "c7f044602e9b",
-                      "f257fde29c7a"
+                      "1ae8bc733a14e374f11767d2ad128d4c891dc43f",
+                      "337fec4d2edcf0e7a467e35f818234bc620068b5",
+                      "c7f044602e9bd5dec6528b33114df3d0221e6359",
+                      "f257fde29c7a847c9b607f6e958656d0df0fb15c"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "de7290d8b885",
-          "rev": 1,
+          "node": "de7290d8b885925115bb9e88887252dfc20ef2a8",
           "shortdescription": "A0"
       }
   ]
@@ -232,8 +231,7 @@
   [
       {
           "markers": [],
-          "node": "c7f044602e9b",
-          "rev": 5,
+          "node": "c7f044602e9bd5dec6528b33114df3d0221e6359",
           "shortdescription": "A0"
       },
       {
@@ -243,23 +241,22 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "parent",
                       "content"
                   ],
                   "operation": "split",
                   "succnodes": [
-                      "1ae8bc733a14",
-                      "337fec4d2edc",
-                      "c7f044602e9b",
-                      "f257fde29c7a"
+                      "1ae8bc733a14e374f11767d2ad128d4c891dc43f",
+                      "337fec4d2edcf0e7a467e35f818234bc620068b5",
+                      "c7f044602e9bd5dec6528b33114df3d0221e6359",
+                      "f257fde29c7a847c9b607f6e958656d0df0fb15c"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "de7290d8b885",
-          "rev": 1,
+          "node": "de7290d8b885925115bb9e88887252dfc20ef2a8",
           "shortdescription": "A0"
       }
   ]
--- a/tests/test-evolve-obshistory-phase-divergent.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-phase-divergent.t	Tue Dec 10 20:47:13 2019 +0100
@@ -102,19 +102,18 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "fdf9bde5129a"
+                      "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
@@ -196,30 +195,28 @@
   [
       {
           "markers": [],
-          "node": "fdf9bde5129a",
-          "rev": 2,
+          "node": "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e",
           "shortdescription": "A1"
       },
       {
           "markers": [
               {
                   "date": [
-                      0,
+                      0.0,
                       0
                   ],
-                  "effect": [
+                  "effects": [
                       "description"
                   ],
                   "operation": "amend",
                   "succnodes": [
-                      "fdf9bde5129a"
+                      "fdf9bde5129a28d4548fadd3f62b265cdd3b7a2e"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
--- a/tests/test-evolve-obshistory-prune.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-prune.t	Tue Dec 10 20:47:13 2019 +0100
@@ -82,8 +82,7 @@
                   "verb": "pruned"
               }
           ],
-          "node": "0dec01379d3b",
-          "rev": 2,
+          "node": "0dec01379d3be6318c470ead31b1fe7ae7cb53d5",
           "shortdescription": "B0"
       }
   ]
@@ -94,8 +93,7 @@
   [
       {
           "markers": [],
-          "node": "471f378eab4c",
-          "rev": 1,
+          "node": "471f378eab4c5e25f6c77f785b27c936efb22874",
           "shortdescription": "A0"
       }
   ]
--- a/tests/test-evolve-obshistory-split.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory-split.t	Tue Dec 10 20:47:13 2019 +0100
@@ -119,22 +119,21 @@
                       *, (glob)
                       0 (glob)
                   ],
-                  "effect": [
+                  "effects": [
                       "parent",
                       "content"
                   ],
                   "note": "testing split",
                   "operation": "split",
                   "succnodes": [
-                      "337fec4d2edc",
-                      "f257fde29c7a"
+                      "337fec4d2edcf0e7a467e35f818234bc620068b5",
+                      "f257fde29c7a847c9b607f6e958656d0df0fb15c"
                   ],
                   "user": "test",
                   "verb": "rewritten"
               }
           ],
-          "node": "471597cad322",
-          "rev": 1,
+          "node": "471597cad322d1f659bb169751be9133dad92ef3",
           "shortdescription": "A0"
       }
   ]
@@ -245,14 +244,18 @@
   
   $ hg obslog -R $TESTTMP/server --no-graph -f --all --patch tip
   f257fde29c7a (2) A0
+  
   471597cad322
     rewritten(parent, content) as 337fec4d2edc, f257fde29c7a using split by test (Thu Jan 01 00:00:00 1970 +0000)
       note: testing split
       (No patch available, context is not local)
+  
 
   $ hg obslog -R $TESTTMP/server --no-graph -f --all --patch tip
   f257fde29c7a (2) A0
+  
   471597cad322
     rewritten(parent, content) as 337fec4d2edc, f257fde29c7a using split by test (Thu Jan 01 00:00:00 1970 +0000)
       note: testing split
       (No patch available, context is not local)
+  
--- a/tests/test-evolve-obshistory.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-obshistory.t	Tue Dec 10 20:47:13 2019 +0100
@@ -168,3 +168,12 @@
        [evolve.verb|rewritten](description) as [evolve.node|fdf9bde5129a] using [evolve.operation|amend] by [evolve.user|test] [evolve.date|(Thu Jan 01 00:00:00 1970 +0000)]
          (No patch available, successor is unknown locally)
   
+
+  $ hg obslog 7a230b46bf61 --graph \
+  > -T '{node|short} {rev} {desc|firstline}\n{markers % "rewritten using {operation}"}\n'
+  o  7a230b46bf61 2 A2
+  |
+  x  fdf9bde5129a
+  |  rewritten using amend
+  @  471f378eab4c 1 A0
+     rewritten using amend
--- a/tests/test-evolve-orphan-merge.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-evolve-orphan-merge.t	Tue Dec 10 20:47:13 2019 +0100
@@ -134,11 +134,18 @@
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     merging a and b
   
+
+Clean up to prepare for next test case
+  $ hg prune -r 64370c9805e7 -r 3d41537b44ca -r 968d205ba4d8
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  working directory is now at 8fa14d15e168
+  3 changesets pruned
+
 2) When merging both the parents resulted in conflicts
 ------------------------------------------------------
 
   $ hg up 8fa14d15e168
-  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ echo foo > c
   $ hg ci -Aqm "foo to c"
   $ hg prev
@@ -152,24 +159,6 @@
   |   () draft
   | o  8:1c165c673853 foo to c
   |/    () draft
-  | o    7:968d205ba4d8 merging a and b
-  | |\    () draft
-  +---o  6:3d41537b44ca added a
-  | |     () draft
-  | o  4:64370c9805e7 added b
-  |/    () draft
-  o  0:8fa14d15e168 added hgignore
-      () draft
-
-Prune old test changesets to have clear graph view
-  $ hg prune -r 64370c9805e7 -r 3d41537b44ca -r 968d205ba4d8
-  3 changesets pruned
-
-  $ hg glog
-  @  9:d0f84b25d4e3 bar to c
-  |   () draft
-  | o  8:1c165c673853 foo to c
-  |/    () draft
   o  0:8fa14d15e168 added hgignore
       () draft
 
@@ -532,21 +521,16 @@
   $ hg evolve -r .
   move:[28] merged l and x
   atop:[0] added hgignore
-  working directory is now at b61ba77b924a
+  evolution of 28:fb8fe870ae7d created no changes to commit
+  working directory is now at 8fa14d15e168
 
   $ hg glog
-  @  29:b61ba77b924a merged l and x
-  |   () draft
-  o  0:8fa14d15e168 added hgignore
+  @  0:8fa14d15e168 added hgignore
       () draft
 
 7) When one parent is pruned without successor and has no parent
 ----------------------------------------------------------------
 
-  $ hg prune -r .
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory is now at 8fa14d15e168
-  1 changesets pruned
   $ hg up null
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
 
@@ -560,9 +544,9 @@
   (branch merge, don't forget to commit)
   $ hg ci -m "merge commit"
   $ hg glog
-  @    31:32beb84b9dbc merge commit
+  @    30:32beb84b9dbc merge commit
   |\    () draft
-  | o  30:f3ba8b99bb6f added foo
+  | o  29:f3ba8b99bb6f added foo
   |     () draft
   o  0:8fa14d15e168 added hgignore
       () draft
@@ -572,9 +556,9 @@
   1 new orphan changesets
 
   $ hg glog
-  @    31:32beb84b9dbc merge commit
+  @    30:32beb84b9dbc merge commit
   |\    () draft
-  | x  30:f3ba8b99bb6f added foo
+  | x  29:f3ba8b99bb6f added foo
   |     () draft
   o  0:8fa14d15e168 added hgignore
       () draft
@@ -592,12 +576,12 @@
 just remove that chain.
 
   $ hg evolve -r .
-  move:[31] merge commit
+  move:[30] merge commit
   atop:[-1] 
   working directory is now at d2a03dd8c951
 
   $ hg glog
-  @  32:d2a03dd8c951 merge commit
+  @  31:d2a03dd8c951 merge commit
   |   () draft
   o  0:8fa14d15e168 added hgignore
       () draft
--- a/tests/test-topic.t	Tue Dec 10 20:35:56 2019 +0100
+++ b/tests/test-topic.t	Tue Dec 10 20:47:13 2019 +0100
@@ -122,10 +122,15 @@
   
   list of commands:
   
-   stack         list all changesets in a topic and other information
+  Change organization:
+  
    topics        View current topic, set current topic, change topic for a set
                  of revisions, or see all topics.
   
+  Change navigation:
+  
+   stack         list all changesets in a topic and other information
+  
   (use 'hg help -v topic' to show built-in aliases and global options)
   $ hg help topics
   hg topics [OPTION]... [-r REV]... [TOPIC]