[PATCH 1 of 1] hgweb: side-by-side comparison functionality

wujek srujek wujek.srujek at googlemail.com
Sat Jun 16 07:24:53 CDT 2012


# HG changeset patch
# User wujek srujek
# Date 1339796087 -7200
# Node ID 42dcd87d2a3472da563003824e19d92380a78a7c
# Parent  622aa57a90b1d1f09b3204458b087de12ce2de82
hgweb: side-by-side comparison functionality

Adds new web command to the core, ``comparison``, which enables colorful
side-by-side change display, which for some might be much easier to work with
than the standard line diff output. The idea how to implement comes from the
SonicHq extension.

The web interface gets a new link to call the comparison functionality. It lets
users configure the amount of context lines around change blocks, or to show
full files - check help (also in this changeset) for details and defaults. The
setting in hgrc can be overridden by adding ``context=<value>`` to the request
query string. The comparison creates addressable lines, so as to enable sharing
links to specific lines, just as standard diff does.

Known limitations:
* the column diff is done against the first parent, just as the standard diff
* this change allows examining diffs for single files only (as I am not sure if
  examining the whole changeset in this way would be helpful)
* syntax highlighting of the output changes is not performed (enabling the
  highlight extension has no influence on it)
* * *
hgweb, comparison: adapts monoblue style templates

diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
--- a/mercurial/help/config.txt
+++ b/mercurial/help/config.txt
@@ -1377,6 +1377,12 @@
 ``errorlog``
     Where to output the error log. Default is stderr.
 
+``comparisoncontext``
+    Number of lines of context to show in side-by-side file comparison. If
+    negative or the value ``full``, whole files are shown. Default is 5.
+    This setting can be overridden by a ``context`` request parameter to the
+    ``comparison`` command, taking the same values.
+
 ``hidden``
     Whether to hide the repository in the hgwebdir index.
     Default is False.
diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py
+++ b/mercurial/hgweb/webcommands.py
@@ -22,7 +22,7 @@
 __all__ = [
    'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
    'manifest', 'tags', 'bookmarks', 'branches', 'summary', 'filediff', 'diff',
-   'annotate', 'filelog', 'archive', 'static', 'graph', 'help',
+   'comparison', 'annotate', 'filelog', 'archive', 'static', 'graph', 'help',
 ]
 
 def log(web, req, tmpl):
@@ -586,6 +586,31 @@
 
 diff = filediff
 
+def comparison(web, req, tmpl):
+    ctx = webutil.changectx(web.repo, req)
+    path = webutil.cleanpath(web.repo, req.form['file'][0])
+    rename = path in ctx and webutil.renamelink(ctx[path]) or []
+
+    parsecontext = lambda v: v == 'full' and -1 or int(v)
+    if 'context' in req.form:
+        context = parsecontext(req.form['context'][0])
+    else:
+        context = parsecontext(web.config('web', 'comparisoncontext', '5'))
+
+    comparison = webutil.compare(tmpl, ctx, path, context)
+    return tmpl('filecomparison',
+                file=path,
+                node=hex(ctx.node()),
+                rev=ctx.rev(),
+                date=ctx.date(),
+                desc=ctx.description(),
+                author=ctx.user(),
+                rename=rename,
+                branch=webutil.nodebranchnodefault(ctx),
+                parent=webutil.parents(ctx),
+                child=webutil.children(ctx),
+                comparison=comparison)
+
 def annotate(web, req, tmpl):
     fctx = webutil.filectx(web.repo, req)
     f = fctx.path()
diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -6,10 +6,11 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import os, copy
+import os, mimetypes, copy
 from mercurial import match, patch, scmutil, error, ui, util
 from mercurial.i18n import _
 from mercurial.node import hex, nullid
+import difflib
 
 def up(p):
     if p[0] != "/":
@@ -220,6 +221,92 @@
     yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
                lines=prettyprintlines(''.join(block), blockno))
 
+def compare(tmpl, ctx, path, context):
+    '''Generator function that provides side-by-side comparison data.'''
+
+    def filelines(f):
+        if util.binary(f.data()):
+            mt = mimetypes.guess_type(f.path())[0]
+            if not mt:
+                mt = 'application/octet-stream'
+            return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
+        return f.data().splitlines()
+
+    def compline(type, leftlineno, leftline, rightlineno, rightline):
+        lineid = leftlineno and ("l%s" % leftlineno) or ''
+        lineid += rightlineno and ("r%s" % rightlineno) or ''
+        return tmpl('comparisonline',
+                    type=type,
+                    lineid=lineid,
+                    leftlinenumber="% 6s" % (leftlineno or ''),
+                    leftline=leftline or '',
+                    rightlinenumber="% 6s" % (rightlineno or ''),
+                    rightline=rightline or '')
+
+    def getblock(opcodes):
+        for type, llo, lhi, rlo, rhi in opcodes:
+            len1 = lhi - llo
+            len2 = rhi - rlo
+            count = min(len1, len2)
+            for i in xrange(count):
+                yield compline(type=type,
+                               leftlineno=llo + i + 1,
+                               leftline=leftlines[llo + i],
+                               rightlineno=rlo + i + 1,
+                               rightline=rightlines[rlo + i])
+            if len1 > len2:
+                for i in xrange(llo + count, lhi):
+                    yield compline(type=type,
+                                   leftlineno=i + 1,
+                                   leftline=leftlines[i],
+                                   rightlineno=None,
+                                   rightline=None)
+            elif len2 > len1:
+                for i in xrange(rlo + count, rhi):
+                    yield compline(type=type,
+                                   leftlineno=None,
+                                   leftline=None,
+                                   rightlineno=i + 1,
+                                   rightline=rightlines[i])
+
+    if path in ctx:
+        fctx = ctx[path]
+        rightrev = fctx.filerev()
+        rightnode = fctx.filenode()
+        rightlines = filelines(fctx)
+        parents = fctx.parents()
+        if not parents:
+            leftrev = -1
+            leftnode = nullid
+            leftlines = ()
+        else:
+            pfctx = parents[0]
+            leftrev = pfctx.filerev()
+            leftnode = pfctx.filenode()
+            leftlines = filelines(pfctx)
+    else:
+        rightrev = -1
+        rightnode = nullid
+        rightlines = ()
+        fctx = ctx.parents()[0][path]
+        leftrev = fctx.filerev()
+        leftnode = fctx.filenode()
+        leftlines = filelines(fctx)
+
+    s = difflib.SequenceMatcher(None, leftlines, rightlines)
+    if context < 0:
+        blocks = [tmpl('comparisonblock', lines=getblock(s.get_opcodes()))]
+    else:
+        blocks = (tmpl('comparisonblock', lines=getblock(oc))
+                     for oc in s.get_grouped_opcodes(n=context))
+
+    yield tmpl('comparison',
+               leftrev=leftrev,
+               leftnode=hex(leftnode),
+               rightrev=rightrev,
+               rightnode=hex(rightnode),
+               blocks=blocks)
+
 def diffstatgen(ctx):
     '''Generator function that provides the diffstat data.'''
 
diff --git a/mercurial/templates/monoblue/fileannotate.tmpl b/mercurial/templates/monoblue/fileannotate.tmpl
--- a/mercurial/templates/monoblue/fileannotate.tmpl
+++ b/mercurial/templates/monoblue/fileannotate.tmpl
@@ -35,6 +35,7 @@
         <li><a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a></li>
         <li class="current">annotate</li>
         <li><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a></li>
+        <li><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a></li>
         <li><a href="{url}raw-annotate/{node|short}/{file|urlescape}">raw</a></li>
     </ul>
 
diff --git a/mercurial/templates/monoblue/filediff.tmpl b/mercurial/templates/monoblue/filecomparison.tmpl
copy from mercurial/templates/monoblue/filediff.tmpl
copy to mercurial/templates/monoblue/filecomparison.tmpl
--- a/mercurial/templates/monoblue/filediff.tmpl
+++ b/mercurial/templates/monoblue/filecomparison.tmpl
@@ -1,5 +1,5 @@
 {header}
-<title>{repo|escape}: diff {file|escape}</title>
+<title>{repo|escape}: comparison {file|escape}</title>
     <link rel="alternate" type="application/atom+xml" href="{url}atom-log" title="Atom feed for {repo|escape}"/>
     <link rel="alternate" type="application/rss+xml" href="{url}rss-log" title="RSS feed for {repo|escape}"/>
 </head>
@@ -7,7 +7,7 @@
 <body>
 <div id="container">
     <div class="page-header">
-        <h1><a href="{url}summary{sessionvars%urlparameter}">{repo|escape}</a> / file diff</h1>
+        <h1><a href="{url}summary{sessionvars%urlparameter}">{repo|escape}</a> / file comparison</h1>
 
         <form action="{url}log">
             {sessionvars%hiddenformentry}
@@ -34,23 +34,31 @@
         <li><a href="{url}file/{node|short}/{file|urlescape}{sessionvars%urlparameter}">file</a></li>
         <li><a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a></li>
         <li><a href="{url}annotate/{node|short}/{file|urlescape}{sessionvars%urlparameter}">annotate</a></li>
-        <li class="current">diff</li>
+        <li><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a></li>
+        <li class="current">comparison</li>
         <li><a href="{url}raw-diff/{node|short}/{file|urlescape}">raw</a></li>
     </ul>
 
-    <h2 class="no-link no-border">diff: {file|escape}</h2>
+    <h2 class="no-link no-border">comparison: {file|escape}</h2>
     <h3 class="changeset">{file|escape}</h3>
 
     <dl class="overview">
         {branch%filerevbranch}
         <dt>changeset {rev}</dt>
         <dd><a href="{url}rev/{node|short}{sessionvars%urlparameter}">{node|short}</a></dd>
-        {parent%filediffparent}
-        {child%filediffchild}
+        {parent%filecompparent}
+        {child%filecompchild}
     </dl>
 
-    <div class="diff">
-    {diff}
+    <div class="legend">
+      <span class="legendinfo equal">equal</span>
+      <span class="legendinfo delete">deleted</span>
+      <span class="legendinfo insert">inserted</span>
+      <span class="legendinfo replace">replaced</span>
+    </div>
+
+    <div class="comparison">
+    {comparison}
     </div>
 
 {footer}
diff --git a/mercurial/templates/monoblue/filediff.tmpl b/mercurial/templates/monoblue/filediff.tmpl
--- a/mercurial/templates/monoblue/filediff.tmpl
+++ b/mercurial/templates/monoblue/filediff.tmpl
@@ -35,6 +35,7 @@
         <li><a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a></li>
         <li><a href="{url}annotate/{node|short}/{file|urlescape}{sessionvars%urlparameter}">annotate</a></li>
         <li class="current">diff</li>
+        <li><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a></li>
         <li><a href="{url}raw-diff/{node|short}/{file|urlescape}">raw</a></li>
     </ul>
 
diff --git a/mercurial/templates/monoblue/filelog.tmpl b/mercurial/templates/monoblue/filelog.tmpl
--- a/mercurial/templates/monoblue/filelog.tmpl
+++ b/mercurial/templates/monoblue/filelog.tmpl
@@ -35,6 +35,7 @@
         <li class="current">revisions</li>
         <li><a href="{url}annotate/{node|short}/{file|urlescape}{sessionvars%urlparameter}">annotate</a></li>
         <li><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a></li>
+        <li><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a></li>
         <li><a href="{url}rss-log/tip/{file|urlescape}">rss</a></li>
     </ul>
 
diff --git a/mercurial/templates/monoblue/filerevision.tmpl b/mercurial/templates/monoblue/filerevision.tmpl
--- a/mercurial/templates/monoblue/filerevision.tmpl
+++ b/mercurial/templates/monoblue/filerevision.tmpl
@@ -35,6 +35,7 @@
         <li><a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a></li>
         <li><a href="{url}annotate/{node|short}/{file|urlescape}{sessionvars%urlparameter}">annotate</a></li>
         <li><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a></li>
+        <li><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a></li>
         <li><a href="{url}raw-file/{node|short}/{file|urlescape}">raw</a></li>
     </ul>
 
diff --git a/mercurial/templates/monoblue/map b/mercurial/templates/monoblue/map
--- a/mercurial/templates/monoblue/map
+++ b/mercurial/templates/monoblue/map
@@ -26,6 +26,7 @@
       <a href="{url}file/{node|short}/{file|urlescape}{sessionvars%urlparameter}">file</a> |
       <a href="{url}annotate/{node|short}/{file|urlescape}{sessionvars%urlparameter}">annotate</a> |
       <a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a> |
+      <a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a> |
       <a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a>
     </td>
   </tr>'
@@ -37,6 +38,7 @@
       file |
       annotate |
       <a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">diff</a> |
+      <a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">comparison</a> |
       <a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">revisions</a>
     </td>
   </tr>'
@@ -74,6 +76,7 @@
 filerevision = filerevision.tmpl
 fileannotate = fileannotate.tmpl
 filediff = filediff.tmpl
+filecomparison = filecomparison.tmpl
 filelog = filelog.tmpl
 fileline = '
   <div style="font-family:monospace" class="parity{parity}">
@@ -94,6 +97,27 @@
 difflineminus = '<span style="color:#cc0000;"><a class="linenr" href="#{lineid}" id="{lineid}">{linenumber}</a> {line|escape}</span>'
 difflineat = '<span style="color:#990099;"><a class="linenr" href="#{lineid}" id="{lineid}">{linenumber}</a> {line|escape}</span>'
 diffline = '<span><a class="linenr" href="#{lineid}" id="{lineid}">{linenumber}</a> {line|escape}</span>'
+
+comparison = '
+  <table class="bigtable">
+    <thead class="header">
+      <tr>
+        <th>{leftrev}:{leftnode|short}</th>
+        <th>{rightrev}:{rightnode|short}</th>
+      </tr>
+    </thead>
+    {blocks}
+  </table>'
+comparisonblock ='
+  <tbody class="block">
+  {lines}
+  </tbody>'
+comparisonline = '
+  <tr>
+    <td class="source {type}"><a class="linenr" href="#{lineid}" id="{lineid}">{leftlinenumber}</a> {leftline|escape}</td>
+    <td class="source {type}"><a class="linenr" href="#{lineid}" id="{lineid}">{rightlinenumber}</a> {rightline|escape}</td>
+  </tr>'
+
 changelogparent = '
   <tr>
     <th class="parent">parent {rev}:</th>
@@ -176,6 +200,9 @@
 filediffparent = '
   <dt>parent {rev}</dt>
   <dd><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{node|short}</a></dd>'
+filecompparent = '
+  <dt>parent {rev}</dt>
+  <dd><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{node|short}</a></dd>'
 filelogparent = '
   <tr>
     <td align="right">parent {rev}: </td>
@@ -184,6 +211,9 @@
 filediffchild = '
   <dt>child {rev}</dt>
   <dd><a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{node|short}</a></dd>'
+filecompchild = '
+  <dt>child {rev}</dt>
+  <dd><a href="{url}comparison/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{node|short}</a></dd>'
 filelogchild = '
   <tr>
     <td align="right">child {rev}: </td>
diff --git a/mercurial/templates/static/style-monoblue.css b/mercurial/templates/static/style-monoblue.css
--- a/mercurial/templates/static/style-monoblue.css
+++ b/mercurial/templates/static/style-monoblue.css
@@ -29,6 +29,7 @@
   background: #FFFFFF;
   position: relative;
   color: #666;
+  overflow: auto;
 }
 
 div.page-header {
@@ -477,3 +478,45 @@
 	position: relative;
 }
 /** end of canvas **/
+
+/** comparison **/
+.legend {
+    margin-left: 20px;
+    padding: 1.5% 0 1.5% 0;
+}
+
+.legendinfo {
+    border: 1px solid #999;
+    font-size: 80%;
+    text-align: center;
+    padding: 0.5%;
+}
+
+.equal {
+    background-color: #ffffff;
+}
+
+.delete {
+    background-color: #ffc5ce;
+}
+
+.insert {
+    background-color: #c5ffc4;
+}
+
+.replace {
+    background-color: #ffff99;
+}
+
+.comparison table td {
+    padding: 0px 5px;
+}
+
+.header th {
+    font-weight: bold;
+}
+
+.block {
+    border-top: 1px solid #999;
+}
+/** end of comparison **/


More information about the Mercurial-devel mailing list