Add critic command and automatic change critiques
authorGregory Szorc <gps@mozilla.com>
Mon, 22 Jul 2013 20:55:49 -0700
changeset 23 38dd4d4b71cd1f2fce363d04026d006780f27f2f
parent 22 a72cc7e9d88127b9cf22a79695ac9701adf095d2
child 24 65f26af28bd40d7c441b2c5a9a1dbc217bcb0624
push id9
push usergszorc@mozilla.com
push dateTue, 23 Jul 2013 03:56:04 +0000
Add critic command and automatic change critiques Only integrates with Python so far, but it's better than nothing.
__init__.py
--- a/__init__.py
+++ b/__init__.py
@@ -66,16 +66,31 @@ Remote References
 When pulling from known Gecko repositories, this extension automatically
 creates references to branches on the remote. These can be referenced via
 the revision <tree>/<name>. e.g. 'central/default'.
 
 Remote refs are read-only and are updated automatically during repository pull
 and push operations.
 
 This feature is similar to Git remote refs.
+
+Static Analysis
+===============
+
+This extension provides static analysis to patches. Currently, only Python
+style checking is performed.
+
+To perform style checking for a single patch, run `hg critic`. By default,
+this will analyze the current working directory. If the working directory is
+clean, the tip changeset will be analyzed. By default, only changed lines are
+reported on.
+
+Static analysis is also performed automatically during qrefresh and commit
+operations. To disable this behavior, add "noautocritic = True" to the
+[mozext] section in your hgrc.
 """
 
 import errno
 import os
 import shutil
 import sys
 
 import mercurial.commands as commands
@@ -91,16 +106,17 @@ from mercurial.error import (
 from mercurial.localrepo import (
     repofilecache,
 )
 from mercurial.node import (
     hex,
 )
 from mercurial import (
     cmdutil,
+    demandimport,
     encoding,
     hg,
     util,
 )
 
 from mozautomation.repository import (
     MercurialRepository,
     resolve_trees_to_official,
@@ -135,16 +151,49 @@ def peerorrepo(ui, path, *args, **kwargs
             raise
 
         path = uri
         return old_peerorrepo(ui, path, *args, **kwargs)
 
 hg._peerorrepo = peerorrepo
 
 
+def critique(ui, repo, entire=False, node=None, **kwargs):
+    """Perform a critique of a changeset."""
+    demandimport.disable()
+
+    try:
+        from flake8.engine import get_style_guide
+    except ImportError:
+        our_dir = os.path.dirname(__file__)
+        for p in ('flake8', 'mccabe', 'pep8', 'pyflakes'):
+            sys.path.insert(0, os.path.join(our_dir, p))
+
+    from flake8.engine import get_style_guide
+    from pep8 import DiffReport, parse_udiff
+
+    style = get_style_guide(parse_argv=False)
+
+    if not entire:
+        diff = ''.join(repo[node].diff())
+        style.options.selected_lines = {}
+        for k, v in parse_udiff(diff).items():
+            if k.startswith('./'):
+                k = k[2:]
+
+            style.options.selected_lines[k] = v
+
+        style.options.report = DiffReport(style.options)
+
+    files = [f for f in repo[node].files() if f.endswith('.py')]
+    style.check_files(files)
+
+    demandimport.enable()
+
+
 @command('moztrees', [], _('hg moztrees'))
 def moztrees(ui, **opts):
     """Show information about Mozilla source trees."""
     from mozautomation.repository import TREE_ALIASES, REPOS
 
     longest = max(len(tree) for tree in REPOS.keys())
     ui.write('%s  %s\n' % (_('Repo').rjust(longest), _('Aliases')))
 
@@ -285,16 +334,32 @@ def tbpl(ui, repo, tree=None, rev=None, 
 
     url = 'https://tbpl.mozilla.org/?tree=%s&rev=%s' % (tree_official,
         push_node[0:12])
 
     import webbrowser
     webbrowser.get('firefox').open(url)
 
 
+@command('critic',
+    [('e', 'entire', False,
+        _('Report on entire file content, not just changed parts'),
+        ''
+    )],
+    _('hg critic [REV]')
+)
+def critic(ui, repo, rev='.', entire=False, **opts):
+    critique(ui, repo, node=rev, entire=entire, **opts)
+
+
+def critic_hook(ui, repo, node=None, **opts):
+    critique(ui, repo, node=node, **opts)
+    return 0
+
+
 class remoterefs(dict):
     """Represents a remote refs file."""
 
     def __init__(self, repo):
         dict.__init__(self)
         self._repo = repo
 
         try:
@@ -390,9 +455,11 @@ def reposetup(ui, repo):
         def _update_remote_refs(self, remote, tree):
             for branch, nodes in remote.branchmap().items():
                 for node in nodes:
                     self.remoterefs['%s/%s' % (tree, branch)] = node
 
             self.remoterefs.write()
 
     repo.__class__ = remotestrackingrepo
-
+    if not ui.configbool('mozext', 'noautocritic'):
+        ui.setconfig('hooks', 'commit.critic', critic_hook)
+        ui.setconfig('hooks', 'qrefresh.critic', critic_hook)