Bug 1281899 - [mozlint] Add ability to lint files touched by revisions and/or the working directory, r=smacleod
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 24 Jun 2016 14:09:58 -0400
changeset 343852 6984d7cf65bbcf19443ef4c8e09e5c7f4e8ae5c3
parent 343851 eaaff060e865adebe50d6877b670db089ca5caf5
child 343853 09d03ee4fd06435e90887f3d345979cad5968e8d
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmacleod
bugs1281899
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1281899 - [mozlint] Add ability to lint files touched by revisions and/or the working directory, r=smacleod This adds two parameters, --rev and --workdir. Each works both with mercurial and git (though the syntax for specifying revisions is different between them). The value is simply forwarded to either |hg log| or |git diff| so syntax like |mach lint -r .~4::.| or |mach lint -r "HEAD~4 HEAD"| will work as expected. MozReview-Commit-ID: aOGp2Yrncs
python/mozlint/mozlint/cli.py
tools/lint/docs/usage.rst
tools/lint/mach_commands.py
--- a/python/mozlint/mozlint/cli.py
+++ b/python/mozlint/mozlint/cli.py
@@ -37,25 +37,80 @@ class MozlintParser(ArgumentParser):
         [['-n', '--no-filter'],
          {'dest': 'use_filters',
           'default': True,
           'action': 'store_false',
           'help': "Ignore all filtering. This is useful for quickly "
                   "testing a directory that otherwise wouldn't be run, "
                   "without needing to modify the config file.",
           }],
+        [['-r', '--rev'],
+         {'default': None,
+          'help': "Lint files touched by the given revision(s). Works with "
+                  "mercurial or git."
+          }],
+        [['-w', '--workdir'],
+         {'default': False,
+          'action': 'store_true',
+          'help': "Lint files touched by changes in the working directory "
+                  "(i.e haven't been committed yet). Works with mercurial or git.",
+          }],
     ]
 
     def __init__(self, **kwargs):
         ArgumentParser.__init__(self, usage=self.__doc__, **kwargs)
 
         for cli, args in self.arguments:
             self.add_argument(*cli, **args)
 
 
+class VCFiles(object):
+    def __init__(self):
+        self._vcs = None
+
+    @property
+    def vcs(self):
+        if self._vcs:
+            return self._vcs
+
+        self._vcs = 'none'
+        with open(os.devnull, 'wb') as DEVNULL:
+            if not subprocess.call(['hg', 'root'], stdout=DEVNULL):
+                self._vcs = 'hg'
+            elif not subprocess.call(['git', 'rev-parse'], stdout=DEVNULL):
+                self._vcs = 'git'
+        return self._vcs
+
+    @property
+    def is_hg(self):
+        return self.vcs == 'hg'
+
+    @property
+    def is_git(self):
+        return self.vcs == 'git'
+
+    def by_rev(self, rev):
+        if self.is_hg:
+            cmd = ['hg', 'log', '-T', '{files % "\\n{file}"}', '-r', rev]
+        elif self.is_git(self):
+            cmd = ['git', 'diff', '--name-only', rev]
+        else:
+            return []
+        return subprocess.check_output(cmd).split()
+
+    def by_workdir(self):
+        if self.is_hg:
+            cmd = ['hg', 'status', '-amn']
+        elif self.is_git(self):
+            cmd = ['git', 'diff', '--name-only']
+        else:
+            return []
+        return subprocess.check_output(cmd).split()
+
+
 def find_linters(self, linters=None):
     lints = []
     for search_path in SEARCH_PATHS:
         if not os.path.isdir(search_path):
             continue
 
         files = os.listdir(search_path)
         for f in files:
@@ -65,18 +120,25 @@ def find_linters(self, linters=None):
 
             if linters and name not in linters:
                 continue
 
             lints.append(os.path.join(search_path, f))
     return lints
 
 
-def run(paths, linters, fmt, **lintargs):
+def run(paths, linters, fmt, rev, workdir, **lintargs):
     from mozlint import LintRoller, formatters
+
+    # Calculate files from VCS
+    vcfiles = VCFiles()
+    if rev:
+        paths.extend(vcfiles.by_rev(rev))
+    if workdir:
+        paths.extend(vcfiles.by_workdir())
     paths = paths or ['.']
 
     lint = LintRoller(**lintargs)
     lint.read(find_linters(linters))
 
     # run all linters
     results = lint.roll(paths)
 
--- a/tools/lint/docs/usage.rst
+++ b/tools/lint/docs/usage.rst
@@ -18,8 +18,24 @@ Multiple paths are allowed:
 against them. For example, if the directory contains both JavaScript and Python files then mozlint
 will automatically run both ESLint and Flake8 against those files respectively.
 
 To restrict which linters are invoked manually, pass in ``-l/--linter``:
 
 .. parsed-literal::
 
     ./mach lint -l eslint path/to/files
+
+Finally, ``mozlint`` can lint the files touched by a set of revisions or the working directory using
+the ``-r/--rev`` and ``-w/--workdir`` arguments respectively. These work both with mercurial and
+git. In the case of ``--rev`` the value is passed directly to the underlying vcs, so normal revision
+specifiers will work. For example, say we want to lint all files touched by the last three commits.
+In mercurial, this would be:
+
+.. parsed-literal::
+
+    ./mach lint -r ".~2::."
+
+In git, this would be:
+
+.. parsed-literal::
+
+    ./mach lint -r "HEAD~2 HEAD"
--- a/tools/lint/mach_commands.py
+++ b/tools/lint/mach_commands.py
@@ -69,22 +69,22 @@ def setup_argument_parser():
 
 @CommandProvider
 class MachCommands(MachCommandBase):
 
     @Command(
         'lint', category='devenv',
         description='Run linters.',
         parser=setup_argument_parser)
-    def lint(self, paths, linters, fmt, **lintargs):
+    def lint(self, *runargs, **lintargs):
         """Run linters."""
         from mozlint import cli
         lintargs['exclude'] = ['obj*']
         cli.SEARCH_PATHS.append(here)
-        return cli.run(paths, linters, fmt, **lintargs)
+        return cli.run(*runargs, **lintargs)
 
     @Command('eslint', category='devenv',
              description='Run eslint or help configure eslint for optimal development.')
     @CommandArgument('-s', '--setup', default=False, action='store_true',
                      help='configure eslint for optimal development.')
     @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html]',
                      help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml,.html]".')
     @CommandArgument('-b', '--binary', default=None,