Patchwork [RFC] hgweb: show explanations for unstable changesets

login
register
mail settings
Submitter Anton Shestakov
Date Feb. 21, 2018, 1:51 p.m.
Message ID <628c839d1eec60db3a45.1519221100@neuro>
Download mbox | patch
Permalink /patch/28184/
State Superseded
Headers show

Comments

Anton Shestakov - Feb. 21, 2018, 1:51 p.m.
# HG changeset patch
# User Anton Shestakov <av6@dwimlabs.net>
# Date 1519199933 -28800
#      Wed Feb 21 15:58:53 2018 +0800
# Node ID 628c839d1eec60db3a4548c5bd7430ff4fc9a5a4
# Parent  aefb75730ea34f545f0756bf8441fc9ae07bf8dc
hgweb: show explanations for unstable changesets

This is a port of evolve's feature of listing all unstable changesets in detail
(`hg evolve --list`).

A couple things I'd like to bikeshed:

- "whyunstable": how to call them? These contain all the information needed to
  explain why certain changesets are unstable, but the best name I've come up
  with is this or "reasonsunstable"

- divergentsets() is only used for this feature (even in evolve), but I don't
  know if it's better to be put into obsutil module instead
Yuya Nishihara - Feb. 24, 2018, 12:49 p.m.
On Wed, 21 Feb 2018 21:51:40 +0800, Anton Shestakov wrote:
> # HG changeset patch
> # User Anton Shestakov <av6@dwimlabs.net>
> # Date 1519199933 -28800
> #      Wed Feb 21 15:58:53 2018 +0800
> # Node ID 628c839d1eec60db3a4548c5bd7430ff4fc9a5a4
> # Parent  aefb75730ea34f545f0756bf8441fc9ae07bf8dc
> hgweb: show explanations for unstable changesets
> 
> This is a port of evolve's feature of listing all unstable changesets in detail
> (`hg evolve --list`).
> 
> A couple things I'd like to bikeshed:
> 
> - "whyunstable": how to call them? These contain all the information needed to
>   explain why certain changesets are unstable, but the best name I've come up
>   with is this or "reasonsunstable"
> 
> - divergentsets() is only used for this feature (even in evolve), but I don't
>   know if it's better to be put into obsutil module instead

(To: Boris) ^^^

> +def divergentsets(repo, ctx):
> +    """Compute sets of commits divergent with a given one"""
> +    cache = {}
> +    base = {}
> +    for n in obsutil.allpredecessors(repo.obsstore, [ctx.node()]):
> +        if n == ctx.node():
> +            # a node can't be a base for divergence with itself
> +            continue
> +        nsuccsets = obsutil.successorssets(repo, n, cache)
> +        for nsuccset in nsuccsets:
> +            if ctx.node() in nsuccset:
> +                # we are only interested in *other* successor sets
> +                continue
> +            if tuple(nsuccset) in base:
> +                # we already know the latest base for this divergency
> +                continue
> +            base[tuple(nsuccset)] = n
> +    return [{'divergentnodes': divset, 'commonpredecessor': b}
> +            for divset, b in base.iteritems()]
> +
> +def whyunstable(repo, ctx):
> +    result = []
> +    if ctx.orphan():
> +        for parent in ctx.parents():
> +            kind = None
> +            if parent.orphan():
> +                kind = 'orphan'
> +            elif parent.obsolete():
> +                kind = 'obsolete'
> +            if kind is not None:
> +                result.append({'instability': 'orphan',
> +                               'reason': '%s parent' % kind,
> +                               'node': parent.hex()})
> +    if ctx.phasedivergent():
> +        predecessors = obsutil.allpredecessors(repo.obsstore, [ctx.node()],
> +                                               ignoreflags=obsolete.bumpedfix)
> +        immutable = [repo[p] for p in predecessors
> +                     if p in repo and not repo[p].mutable()]
> +        for predecessor in immutable:
> +            result.append({'instability': 'phase-divergent',
> +                           'reason': 'immutable predecessor',
> +                           'node': predecessor.hex()})
> +    if ctx.contentdivergent():
> +        dsets = divergentsets(repo, ctx)
> +        for dset in dsets:
> +            divnodes = _siblings(repo[n] for n in dset['divergentnodes'])
> +            result.append({'instability': 'content-divergent',
> +                           'divergentnodes': divnodes,
> +                           'reason': 'predecessor',
> +                           'node': hex(dset['commonpredecessor'])})
> +    return result

Perhaps these functions should be (mostly) moved to obsutil, and preferably
tested without using a web ui.
Boris Feld - Feb. 26, 2018, 11:45 a.m.
On 21/02/2018 14:51, Anton Shestakov wrote:
> # HG changeset patch
> # User Anton Shestakov <av6@dwimlabs.net>
> # Date 1519199933 -28800
> #      Wed Feb 21 15:58:53 2018 +0800
> # Node ID 628c839d1eec60db3a4548c5bd7430ff4fc9a5a4
> # Parent  aefb75730ea34f545f0756bf8441fc9ae07bf8dc
> hgweb: show explanations for unstable changesets
>
> This is a port of evolve's feature of listing all unstable changesets in detail
> (`hg evolve --list`).
>
> A couple things I'd like to bikeshed:
>
> - "whyunstable": how to call them? These contain all the information needed to
>    explain why certain changesets are unstable, but the best name I've come up
>    with is this or "reasonsunstable"
unstablereasons or unstablesources seems fine.
> - divergentsets() is only used for this feature (even in evolve), but I don't
>    know if it's better to be put into obsutil module instead
Moving them to obsutils seems to be the way to go.

As yuya pointed out, it would be nice to have not web-UI dependant 
tests. The simplest way seems to be by adding debug command. However, a 
cleaner way may be to add new templates. I'm not sure how such template 
would work (because we did not think about it much). Maybe a 
{unstabilitiesdetailed} template or maybe a filter {unstabilities|details}.
>
> diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
> --- a/mercurial/hgweb/webutil.py
> +++ b/mercurial/hgweb/webutil.py
> @@ -28,6 +28,8 @@ from .. import (
>       error,
>       match,
>       mdiff,
> +    obsolete,
> +    obsutil,
>       patch,
>       pathutil,
>       pycompat,
> @@ -358,6 +360,58 @@ def succsandmarkers(repo, ctx):
>                                          for successor in item['successors'])
>           yield item
>   
> +def divergentsets(repo, ctx):
> +    """Compute sets of commits divergent with a given one"""
> +    cache = {}
> +    base = {}
> +    for n in obsutil.allpredecessors(repo.obsstore, [ctx.node()]):
> +        if n == ctx.node():
> +            # a node can't be a base for divergence with itself
> +            continue
> +        nsuccsets = obsutil.successorssets(repo, n, cache)
> +        for nsuccset in nsuccsets:
> +            if ctx.node() in nsuccset:
> +                # we are only interested in *other* successor sets
> +                continue
> +            if tuple(nsuccset) in base:
> +                # we already know the latest base for this divergency
> +                continue
> +            base[tuple(nsuccset)] = n
> +    return [{'divergentnodes': divset, 'commonpredecessor': b}
> +            for divset, b in base.iteritems()]
> +
> +def whyunstable(repo, ctx):
> +    result = []
> +    if ctx.orphan():
> +        for parent in ctx.parents():
> +            kind = None
> +            if parent.orphan():
> +                kind = 'orphan'
> +            elif parent.obsolete():
> +                kind = 'obsolete'
> +            if kind is not None:
> +                result.append({'instability': 'orphan',
> +                               'reason': '%s parent' % kind,
> +                               'node': parent.hex()})
> +    if ctx.phasedivergent():
> +        predecessors = obsutil.allpredecessors(repo.obsstore, [ctx.node()],
> +                                               ignoreflags=obsolete.bumpedfix)
> +        immutable = [repo[p] for p in predecessors
> +                     if p in repo and not repo[p].mutable()]
> +        for predecessor in immutable:
> +            result.append({'instability': 'phase-divergent',
> +                           'reason': 'immutable predecessor',
> +                           'node': predecessor.hex()})
> +    if ctx.contentdivergent():
> +        dsets = divergentsets(repo, ctx)
> +        for dset in dsets:
> +            divnodes = _siblings(repo[n] for n in dset['divergentnodes'])
> +            result.append({'instability': 'content-divergent',
> +                           'divergentnodes': divnodes,
> +                           'reason': 'predecessor',
> +                           'node': hex(dset['commonpredecessor'])})
> +    return result
> +
>   def commonentry(repo, ctx):
>       node = ctx.node()
>       return {
> @@ -371,6 +425,7 @@ def commonentry(repo, ctx):
>           'obsolete': ctx.obsolete(),
>           'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
>           'instabilities': [{"instability": i} for i in ctx.instabilities()],
> +        'whyunstable': lambda **x: whyunstable(repo, ctx),
>           'branch': nodebranchnodefault(ctx),
>           'inbranch': nodeinbranch(repo, ctx),
>           'branches': nodebranchdict(repo, ctx),
> diff --git a/mercurial/templates/paper/changeset.tmpl b/mercurial/templates/paper/changeset.tmpl
> --- a/mercurial/templates/paper/changeset.tmpl
> +++ b/mercurial/templates/paper/changeset.tmpl
> @@ -53,6 +53,10 @@
>    <th>obsolete</th>
>    <td>{join(succsandmarkers%obsfateentry, '<br>\n')}</td>
>   </tr>')}
> +{if(instabilities, '<tr>
> + <th>unstable</th>
> + <td>{join(whyunstable%whyunstableentry, '<br>\n')}</td>
> +</tr>')}
>   <tr>
>    <th class="author">parents</th>
>    <td class="author">{ifeq(count(parent), '2', parent%changesetparentdiff, parent%changesetparent)}</td>
> diff --git a/mercurial/templates/paper/map b/mercurial/templates/paper/map
> --- a/mercurial/templates/paper/map
> +++ b/mercurial/templates/paper/map
> @@ -216,6 +216,9 @@ obsfateoperations = '{if(obsfateoperatio
>   obsfateusers = '{if(obsfateusers(markers), ' by {join(obsfateusers(markers)%'{user|obfuscate}', ', ')}')}'
>   obsfatedate = '{if(obsfatedate(markers), ' {ifeq(min(obsfatedate(markers)), max(obsfatedate(markers)), '<span class="age">{min(obsfatedate(markers))|rfc822date}</span>', 'between <span class="age">{min(obsfatedate(markers))|rfc822date}</span> and <span class="age">{max(obsfatedate(markers))|rfc822date}</span>')}')}'
>   obsfateentry = '{obsfateverb}{obsfateoperations}{obsfatesuccessors}{obsfateusers}{obsfatedate}'
> +instabilitychangesetlink = '<a href="{url|urlescape}rev/{node|short}{sessionvars%urlparameter}">{node|short}</a>'
> +divergentnode = '{instabilitychangesetlink} ({phase})'
> +whyunstableentry = '{instability}: {if(divergentnodes, divergentnodes%divergentnode)} {reason} {instabilitychangesetlink}'
>   
>   filediffparent = '
>     <tr>
> diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
> --- a/tests/test-hgweb-commands.t
> +++ b/tests/test-hgweb-commands.t
> @@ -903,6 +903,7 @@ Logs and changes
>      <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
>     </tr>
>     
> +
>     <tr>
>      <th class="author">parents</th>
>      <td class="author"></td>
> diff --git a/tests/test-hgweb-diffs.t b/tests/test-hgweb-diffs.t
> --- a/tests/test-hgweb-diffs.t
> +++ b/tests/test-hgweb-diffs.t
> @@ -104,6 +104,7 @@ revision
>      <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
>     </tr>
>     
> +
>     <tr>
>      <th class="author">parents</th>
>      <td class="author"></td>
> @@ -400,6 +401,7 @@ revision
>      <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
>     </tr>
>     
> +
>     <tr>
>      <th class="author">parents</th>
>      <td class="author"></td>
> diff --git a/tests/test-hgweb-removed.t b/tests/test-hgweb-removed.t
> --- a/tests/test-hgweb-removed.t
> +++ b/tests/test-hgweb-removed.t
> @@ -85,6 +85,7 @@ revision
>      <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
>     </tr>
>     
> +
>     <tr>
>      <th class="author">parents</th>
>      <td class="author"><a href="/rev/cb9a9f314b8b">cb9a9f314b8b</a> </td>
> diff --git a/tests/test-obsolete-divergent.t b/tests/test-obsolete-divergent.t
> --- a/tests/test-obsolete-divergent.t
> +++ b/tests/test-obsolete-divergent.t
> @@ -717,3 +717,19 @@ Use scmutil.cleanupnodes API to create d
>     a178212c3433c4e77b573f6011e29affb8aefa33 1a2a9b5b0030632400aa78e00388c20f99d3ec44 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '1', 'operation': 'amend', 'user': 'test'}
>     a178212c3433c4e77b573f6011e29affb8aefa33 ad6478fb94ecec98b86daae98722865d494ac561 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '13', 'operation': 'test', 'user': 'test'}
>     ad6478fb94ecec98b86daae98722865d494ac561 70d5a63ca112acb3764bc1d7320ca90ea688d671 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '9', 'operation': 'test', 'user': 'test'}
> +
> +#if serve
> +
> +  $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
> +  $ cat hg.pid >> $DAEMON_PIDS
> +
> +check explanation for a content-divergent changeset
> +
> +  $ get-with-headers.py localhost:$HGPORT 'rev/1a2a9b5b0030?style=paper' | grep divergent:
> +   <td>content-divergent: <a href="/rev/70d5a63ca112?style=paper">70d5a63ca112</a> (draft) predecessor <a href="/rev/a178212c3433?style=paper">a178212c3433</a></td>
> +  $ get-with-headers.py localhost:$HGPORT 'rev/1a2a9b5b0030?style=coal' | grep divergent:
> +   <td>content-divergent: <a href="/rev/70d5a63ca112?style=coal">70d5a63ca112</a> (draft) predecessor <a href="/rev/a178212c3433?style=coal">a178212c3433</a></td>
> +
> +  $ killdaemons.py
> +
> +#endif
> diff --git a/tests/test-obsolete.t b/tests/test-obsolete.t
> --- a/tests/test-obsolete.t
> +++ b/tests/test-obsolete.t
> @@ -1066,9 +1066,19 @@ check changeset with instabilities
>       <th class="instabilities">instabilities:</th>
>       <td class="instabilities">orphan phase-divergent </td>
>   
> +check explanation for an orphan and phase-divergent changeset
> +
> +  $ get-with-headers.py localhost:$HGPORT 'rev/50c51b361e60?style=paper' | egrep '(orphan|phase-divergent):'
> +   <td>orphan:  obsolete parent <a href="/rev/3de5eca88c00?style=paper">3de5eca88c00</a><br>
> +  phase-divergent:  immutable predecessor <a href="/rev/245bde4270cd?style=paper">245bde4270cd</a></td>
> +  $ get-with-headers.py localhost:$HGPORT 'rev/50c51b361e60?style=coal' | egrep '(orphan|phase-divergent):'
> +   <td>orphan:  obsolete parent <a href="/rev/3de5eca88c00?style=coal">3de5eca88c00</a><br>
> +  phase-divergent:  immutable predecessor <a href="/rev/245bde4270cd?style=coal">245bde4270cd</a></td>
> +
>     $ killdaemons.py
>   
>     $ rm hg.pid access.log errors.log
> +
>   #endif
>   
>   Test incoming/outcoming with changesets obsoleted remotely, known locally
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

Patch

diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -28,6 +28,8 @@  from .. import (
     error,
     match,
     mdiff,
+    obsolete,
+    obsutil,
     patch,
     pathutil,
     pycompat,
@@ -358,6 +360,58 @@  def succsandmarkers(repo, ctx):
                                        for successor in item['successors'])
         yield item
 
+def divergentsets(repo, ctx):
+    """Compute sets of commits divergent with a given one"""
+    cache = {}
+    base = {}
+    for n in obsutil.allpredecessors(repo.obsstore, [ctx.node()]):
+        if n == ctx.node():
+            # a node can't be a base for divergence with itself
+            continue
+        nsuccsets = obsutil.successorssets(repo, n, cache)
+        for nsuccset in nsuccsets:
+            if ctx.node() in nsuccset:
+                # we are only interested in *other* successor sets
+                continue
+            if tuple(nsuccset) in base:
+                # we already know the latest base for this divergency
+                continue
+            base[tuple(nsuccset)] = n
+    return [{'divergentnodes': divset, 'commonpredecessor': b}
+            for divset, b in base.iteritems()]
+
+def whyunstable(repo, ctx):
+    result = []
+    if ctx.orphan():
+        for parent in ctx.parents():
+            kind = None
+            if parent.orphan():
+                kind = 'orphan'
+            elif parent.obsolete():
+                kind = 'obsolete'
+            if kind is not None:
+                result.append({'instability': 'orphan',
+                               'reason': '%s parent' % kind,
+                               'node': parent.hex()})
+    if ctx.phasedivergent():
+        predecessors = obsutil.allpredecessors(repo.obsstore, [ctx.node()],
+                                               ignoreflags=obsolete.bumpedfix)
+        immutable = [repo[p] for p in predecessors
+                     if p in repo and not repo[p].mutable()]
+        for predecessor in immutable:
+            result.append({'instability': 'phase-divergent',
+                           'reason': 'immutable predecessor',
+                           'node': predecessor.hex()})
+    if ctx.contentdivergent():
+        dsets = divergentsets(repo, ctx)
+        for dset in dsets:
+            divnodes = _siblings(repo[n] for n in dset['divergentnodes'])
+            result.append({'instability': 'content-divergent',
+                           'divergentnodes': divnodes,
+                           'reason': 'predecessor',
+                           'node': hex(dset['commonpredecessor'])})
+    return result
+
 def commonentry(repo, ctx):
     node = ctx.node()
     return {
@@ -371,6 +425,7 @@  def commonentry(repo, ctx):
         'obsolete': ctx.obsolete(),
         'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
         'instabilities': [{"instability": i} for i in ctx.instabilities()],
+        'whyunstable': lambda **x: whyunstable(repo, ctx),
         'branch': nodebranchnodefault(ctx),
         'inbranch': nodeinbranch(repo, ctx),
         'branches': nodebranchdict(repo, ctx),
diff --git a/mercurial/templates/paper/changeset.tmpl b/mercurial/templates/paper/changeset.tmpl
--- a/mercurial/templates/paper/changeset.tmpl
+++ b/mercurial/templates/paper/changeset.tmpl
@@ -53,6 +53,10 @@ 
  <th>obsolete</th>
  <td>{join(succsandmarkers%obsfateentry, '<br>\n')}</td>
 </tr>')}
+{if(instabilities, '<tr>
+ <th>unstable</th>
+ <td>{join(whyunstable%whyunstableentry, '<br>\n')}</td>
+</tr>')}
 <tr>
  <th class="author">parents</th>
  <td class="author">{ifeq(count(parent), '2', parent%changesetparentdiff, parent%changesetparent)}</td>
diff --git a/mercurial/templates/paper/map b/mercurial/templates/paper/map
--- a/mercurial/templates/paper/map
+++ b/mercurial/templates/paper/map
@@ -216,6 +216,9 @@  obsfateoperations = '{if(obsfateoperatio
 obsfateusers = '{if(obsfateusers(markers), ' by {join(obsfateusers(markers)%'{user|obfuscate}', ', ')}')}'
 obsfatedate = '{if(obsfatedate(markers), ' {ifeq(min(obsfatedate(markers)), max(obsfatedate(markers)), '<span class="age">{min(obsfatedate(markers))|rfc822date}</span>', 'between <span class="age">{min(obsfatedate(markers))|rfc822date}</span> and <span class="age">{max(obsfatedate(markers))|rfc822date}</span>')}')}'
 obsfateentry = '{obsfateverb}{obsfateoperations}{obsfatesuccessors}{obsfateusers}{obsfatedate}'
+instabilitychangesetlink = '<a href="{url|urlescape}rev/{node|short}{sessionvars%urlparameter}">{node|short}</a>'
+divergentnode = '{instabilitychangesetlink} ({phase})'
+whyunstableentry = '{instability}: {if(divergentnodes, divergentnodes%divergentnode)} {reason} {instabilitychangesetlink}'
 
 filediffparent = '
   <tr>
diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
--- a/tests/test-hgweb-commands.t
+++ b/tests/test-hgweb-commands.t
@@ -903,6 +903,7 @@  Logs and changes
    <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
   </tr>
   
+  
   <tr>
    <th class="author">parents</th>
    <td class="author"></td>
diff --git a/tests/test-hgweb-diffs.t b/tests/test-hgweb-diffs.t
--- a/tests/test-hgweb-diffs.t
+++ b/tests/test-hgweb-diffs.t
@@ -104,6 +104,7 @@  revision
    <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
   </tr>
   
+  
   <tr>
    <th class="author">parents</th>
    <td class="author"></td>
@@ -400,6 +401,7 @@  revision
    <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
   </tr>
   
+  
   <tr>
    <th class="author">parents</th>
    <td class="author"></td>
diff --git a/tests/test-hgweb-removed.t b/tests/test-hgweb-removed.t
--- a/tests/test-hgweb-removed.t
+++ b/tests/test-hgweb-removed.t
@@ -85,6 +85,7 @@  revision
    <td class="date age">Thu, 01 Jan 1970 00:00:00 +0000</td>
   </tr>
   
+  
   <tr>
    <th class="author">parents</th>
    <td class="author"><a href="/rev/cb9a9f314b8b">cb9a9f314b8b</a> </td>
diff --git a/tests/test-obsolete-divergent.t b/tests/test-obsolete-divergent.t
--- a/tests/test-obsolete-divergent.t
+++ b/tests/test-obsolete-divergent.t
@@ -717,3 +717,19 @@  Use scmutil.cleanupnodes API to create d
   a178212c3433c4e77b573f6011e29affb8aefa33 1a2a9b5b0030632400aa78e00388c20f99d3ec44 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '1', 'operation': 'amend', 'user': 'test'}
   a178212c3433c4e77b573f6011e29affb8aefa33 ad6478fb94ecec98b86daae98722865d494ac561 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '13', 'operation': 'test', 'user': 'test'}
   ad6478fb94ecec98b86daae98722865d494ac561 70d5a63ca112acb3764bc1d7320ca90ea688d671 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '9', 'operation': 'test', 'user': 'test'}
+
+#if serve
+
+  $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
+  $ cat hg.pid >> $DAEMON_PIDS
+
+check explanation for a content-divergent changeset
+
+  $ get-with-headers.py localhost:$HGPORT 'rev/1a2a9b5b0030?style=paper' | grep divergent:
+   <td>content-divergent: <a href="/rev/70d5a63ca112?style=paper">70d5a63ca112</a> (draft) predecessor <a href="/rev/a178212c3433?style=paper">a178212c3433</a></td>
+  $ get-with-headers.py localhost:$HGPORT 'rev/1a2a9b5b0030?style=coal' | grep divergent:
+   <td>content-divergent: <a href="/rev/70d5a63ca112?style=coal">70d5a63ca112</a> (draft) predecessor <a href="/rev/a178212c3433?style=coal">a178212c3433</a></td>
+
+  $ killdaemons.py
+
+#endif
diff --git a/tests/test-obsolete.t b/tests/test-obsolete.t
--- a/tests/test-obsolete.t
+++ b/tests/test-obsolete.t
@@ -1066,9 +1066,19 @@  check changeset with instabilities
     <th class="instabilities">instabilities:</th>
     <td class="instabilities">orphan phase-divergent </td>
 
+check explanation for an orphan and phase-divergent changeset
+
+  $ get-with-headers.py localhost:$HGPORT 'rev/50c51b361e60?style=paper' | egrep '(orphan|phase-divergent):'
+   <td>orphan:  obsolete parent <a href="/rev/3de5eca88c00?style=paper">3de5eca88c00</a><br>
+  phase-divergent:  immutable predecessor <a href="/rev/245bde4270cd?style=paper">245bde4270cd</a></td>
+  $ get-with-headers.py localhost:$HGPORT 'rev/50c51b361e60?style=coal' | egrep '(orphan|phase-divergent):'
+   <td>orphan:  obsolete parent <a href="/rev/3de5eca88c00?style=coal">3de5eca88c00</a><br>
+  phase-divergent:  immutable predecessor <a href="/rev/245bde4270cd?style=coal">245bde4270cd</a></td>
+
   $ killdaemons.py
 
   $ rm hg.pid access.log errors.log
+
 #endif
 
 Test incoming/outcoming with changesets obsoleted remotely, known locally